mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-18 08:14:06 +00:00
fix: more stability for durable tasks, retry strategy, refactor dead
code
This commit is contained in:
1 parent
114585cd7d
commit
e96ff8658c
27 files changed
+1060
-162
No files matched your search
@@ -65,6 +65,12 @@ const ConversationBubble = forwardRef<
|
||||
index?: number,
|
||||
) => void;
|
||||
filesAttached?: { id: string; fileName: string }[];
|
||||
/**
|
||||
* Every artifact in the conversation, for resolving inline links. Refs
|
||||
* are conversation-scoped, so a link may point at an earlier turn's file.
|
||||
* The chip RAIL still uses this turn's artifacts only.
|
||||
*/
|
||||
conversationArtifacts?: ReturnType<typeof deriveArtifactChips>;
|
||||
onOpenArtifact?: (artifact: { id: string; toolName: string }) => void;
|
||||
onToolAction?: (
|
||||
callId: string,
|
||||
@@ -92,6 +98,7 @@ const ConversationBubble = forwardRef<
|
||||
isStreaming,
|
||||
handleUpdatedQuestionSubmission,
|
||||
filesAttached,
|
||||
conversationArtifacts,
|
||||
onOpenArtifact,
|
||||
onToolAction,
|
||||
agentId,
|
||||
@@ -431,7 +438,7 @@ const ConversationBubble = forwardRef<
|
||||
// A research run already narrates itself above; the status line
|
||||
// would be a second live indicator away from the point of action.
|
||||
suppressStatusLine={Boolean(research)}
|
||||
artifacts={completedArtifacts}
|
||||
artifacts={conversationArtifacts ?? completedArtifacts}
|
||||
onOpenArtifact={onOpenArtifact}
|
||||
renderApproval={(toolCall: ToolCallsType) => (
|
||||
<div className="fade-in mt-4 mr-5 ml-6">
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
MessageScrollerViewport,
|
||||
} from '../components/ui/message-scroller';
|
||||
import Hero from '../Hero';
|
||||
import { deriveArtifactChips } from './artifactChips';
|
||||
import ConversationBubble from './ConversationBubble';
|
||||
import { FEEDBACK, Query, Status } from './conversationModels';
|
||||
import StreamingStatusLine from './StreamingStatusLine';
|
||||
@@ -73,6 +75,15 @@ export default function ConversationMessages({
|
||||
}: ConversationMessagesProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Artifact refs (A1, A2, ...) are CONVERSATION-scoped on the backend — a
|
||||
// ref minted on turn 1 still resolves, and edit_artifact still accepts it,
|
||||
// on turn 5. Resolving inline links against only the current turn's
|
||||
// artifacts made "the CSV from earlier" render as dead plain text.
|
||||
const conversationArtifacts = useMemo(
|
||||
() => queries.flatMap((query) => deriveArtifactChips(query.tool_calls)),
|
||||
[queries],
|
||||
);
|
||||
|
||||
const hasMessages = queries.length > 0;
|
||||
const lastQuery = queries[queries.length - 1];
|
||||
const lastTurnContentLength =
|
||||
@@ -212,6 +223,7 @@ export default function ConversationMessages({
|
||||
segments={query.segments}
|
||||
workflowRunId={query.workflow_run_id}
|
||||
research={query.research}
|
||||
conversationArtifacts={conversationArtifacts}
|
||||
onOpenArtifact={onOpenArtifact}
|
||||
onToolAction={onToolAction}
|
||||
feedback={query.feedback}
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
*
|
||||
* Payloads captured verbatim from `GET /api/get_single_conversation` against a
|
||||
* live run: turn 1's `run_code` wrote a CSV and a PNG (refs A1/A2), turn 2 wrote
|
||||
* a JSON summary (ref A3). Refs are numbered per *conversation*, so turn 2's
|
||||
* only artifact is A3 — indexing a turn's artifact list by the ref number picks
|
||||
* the wrong file, or none.
|
||||
* a JSON summary (ref A3). Refs are numbered per *conversation*, so indexing a
|
||||
* turn's artifact list by the ref number picks the wrong file, or none.
|
||||
*
|
||||
* `resolveSandboxLink` matches on the ref itself, so what it can resolve is
|
||||
* decided entirely by the list it is handed. Handed one turn's artifacts it
|
||||
* degrades an earlier turn's ref to plain text — safe, but wrong, since the
|
||||
* ref is still live conversation-wide. `ConversationMessages` therefore passes
|
||||
* the whole conversation's artifacts; both cases are pinned below.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
@@ -46,13 +51,49 @@ describe('conversation-scoped refs against real turn payloads', () => {
|
||||
|
||||
// The bug: A1 is turn 1's CSV. Positional resolution made it index 0 of
|
||||
// turn 2's list — the JSON summary — and opened the wrong file silently.
|
||||
it('does not hand turn 1s ref to turn 2s file', () => {
|
||||
// Never resolving beats resolving to the wrong file, so a list that lacks
|
||||
// the ref must still degrade to plain text.
|
||||
it('never substitutes a different file for a ref it does not hold', () => {
|
||||
expect(resolveSandboxLink('artifact:A1', turn2)).toEqual({ kind: 'plain' });
|
||||
expect(resolveSandboxLink('sandbox:/artifact/A2', turn2)).toEqual({
|
||||
kind: 'plain',
|
||||
});
|
||||
});
|
||||
|
||||
// ...but the ref IS still live: `edit_artifact` accepts A1 on turn 2, and
|
||||
// the model is told so. Given the conversation's artifacts — what
|
||||
// `ConversationMessages` actually passes — the cross-turn link resolves.
|
||||
it('resolves an earlier turns ref against the whole conversation', () => {
|
||||
const conversation = [...turn1, ...turn2];
|
||||
expect(resolveSandboxLink('artifact:A1', conversation)).toEqual({
|
||||
kind: 'artifact',
|
||||
artifact: turn1[0],
|
||||
});
|
||||
expect(resolveSandboxLink('sandbox:/artifact/A2', conversation)).toEqual({
|
||||
kind: 'artifact',
|
||||
artifact: turn1[1],
|
||||
});
|
||||
expect(resolveSandboxLink('artifact:A3', conversation)).toEqual({
|
||||
kind: 'artifact',
|
||||
artifact: turn2[0],
|
||||
});
|
||||
});
|
||||
|
||||
// Last-write-wins must survive widening: a filename re-written on a later
|
||||
// turn resolves to the newest artifact, not the first match.
|
||||
it('keeps last-write-wins across the conversation', () => {
|
||||
const rewritten: SandboxArtifact = {
|
||||
id: 'a1111111-2222-3333-4444-555555555555',
|
||||
label: 'monthly_sales.csv',
|
||||
ref: 'A4',
|
||||
toolName: 'code_executor',
|
||||
};
|
||||
const conversation = [...turn1, ...turn2, rewritten];
|
||||
expect(
|
||||
resolveSandboxLink('sandbox:/mnt/data/monthly_sales.csv', conversation),
|
||||
).toEqual({ kind: 'artifact', artifact: rewritten });
|
||||
});
|
||||
|
||||
it('still resolves both of turn 1s files on turn 1', () => {
|
||||
expect(resolveSandboxLink('sandbox:/artifact/A1', turn1)).toEqual({
|
||||
kind: 'artifact',
|
||||
|
||||
Reference in new issue
Block a user