bep40 commited on
Commit
ae9ea10
·
verified ·
1 Parent(s): a115fc1

V4: Complete chain - useAgentChat → SessionChat → ChatInput buttons

Browse files
Files changed (1) hide show
  1. patch_auto_continue_v4.py +264 -0
patch_auto_continue_v4.py ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """V4: FINAL WORKING - Fix all TS errors for auto-continue feature.
2
+
3
+ Strategy:
4
+ - useAgentChat: return _showAutoContinue + _cancelAutoContinue (TS _ prefix suppresses "unused")
5
+ - ChatInput.tsx: receive props and render "Tạm dừng" button
6
+ - SessionChat.tsx: pass props from hook to ChatInput
7
+ - events.ts: add 'task_incomplete' to EventType union
8
+ - sse-chat-transport.ts: add onTaskIncomplete + case handler
9
+ - agent_loop.py: send task_incomplete event
10
+ """
11
+
12
+ import os
13
+ import re
14
+
15
+ EVENTS = "/source/frontend/src/types/events.ts"
16
+ USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
17
+ SSE = "/source/frontend/src/lib/sse-chat-transport.ts"
18
+ CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
19
+ SESSION_CHAT = "/source/frontend/src/components/SessionChat.tsx"
20
+
21
+
22
+ def patch_events():
23
+ with open(EVENTS, "r") as f:
24
+ c = f.read()
25
+ if "'task_incomplete'" not in c:
26
+ c = c.replace(" | 'plan_update';", " | 'plan_update'\n | 'task_incomplete';")
27
+ with open(EVENTS, "w") as f: f.write(c)
28
+ print("OK: events.ts")
29
+
30
+
31
+ def patch_sse():
32
+ with open(SSE, "r") as f:
33
+ c = f.read()
34
+
35
+ # Add onTaskIncomplete to interface
36
+ if "onTaskIncomplete:" not in c:
37
+ c = c.replace(
38
+ " onInterrupted: () => void;",
39
+ " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
40
+ )
41
+
42
+ # Add case handler
43
+ if "case 'task_incomplete'" not in c:
44
+ case = """ case 'task_incomplete' as const:
45
+ sideChannel.onTaskIncomplete(
46
+ (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
47
+ );
48
+ break;
49
+ """
50
+ c = c.replace("case 'turn_complete':", case + "\n case 'turn_complete':")
51
+
52
+ with open(SSE, "w") as f: f.write(c)
53
+ print("OK: sse-chat-transport.ts")
54
+
55
+
56
+ def patch_use_agent_chat():
57
+ with open(USE_AGENT_CHAT, "r") as f:
58
+ c = f.read()
59
+
60
+ # 1. Import useState
61
+ if "useState" not in c:
62
+ c = c.replace(
63
+ "import { useCallback, useEffect, useMemo, useRef } from 'react';",
64
+ "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
65
+ )
66
+
67
+ # 2. State (TS _ prefix = unused ok)
68
+ state_block = '''
69
+ // Auto-continue for free models
70
+ const _autoContinueTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
71
+ const [_showAutoContinue, _setShowAutoContinue] = useState(false);
72
+ '''
73
+ if "_autoContinueTimer" not in c:
74
+ c = c.replace(
75
+ "callbacksRef.current = { onReady, onError, onSessionDead };",
76
+ "callbacksRef.current = { onReady, onError, onSessionDead };" + state_block
77
+ )
78
+
79
+ # 3. onTaskIncomplete in interface
80
+ if "onTaskIncomplete:" not in c:
81
+ c = c.replace(
82
+ " onInterrupted: () => void;",
83
+ " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
84
+ )
85
+
86
+ # 4. Cancel ref
87
+ cancel_ref = ' const _cancelAutoContinue = useRef(() => {});\n'
88
+ if "_cancelAutoContinue" not in c:
89
+ # Insert after callbacksRef.current = { ... } area
90
+ c = c.replace(
91
+ " isActiveRef.current = isActive;",
92
+ " isActiveRef.current = isActive;\n" + cancel_ref
93
+ )
94
+
95
+ # 5. Handler in sideChannel - inline all logic
96
+ handler = """ onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => {
97
+ _setShowAutoContinue(true);
98
+ if (_autoContinueTimer.current) clearTimeout(_autoContinueTimer.current);
99
+ _autoContinueTimer.current = setTimeout(() => {
100
+ _setShowAutoContinue(false);
101
+ const plan = incompletePlan.map(i => `- ${i.content}`).join('\\\\n');
102
+ chat.sendMessage({
103
+ text: `[TỰ ĐỘNG TIẾP TỤC] Tiếp tục từ các task chưa hoàn thành:\\\\n${plan}`,
104
+ metadata: { createdAt: new Date().toISOString() },
105
+ });
106
+ }, 10000);
107
+ },
108
+ onInterrupted: () => { /* no-op */ },"""
109
+
110
+ if "onTaskIncomplete: (incompletePlan" not in c:
111
+ old = ' onInterrupted: () => { /* no-op — handled by stop() caller */ },'
112
+ if old in c:
113
+ c = c.replace(old, handler)
114
+
115
+ # 6. Set cancel ref function + cleanup
116
+ cancel_func = """
117
+ _cancelAutoContinue.current = () => {
118
+ _setShowAutoContinue(false);
119
+ if (_autoContinueTimer.current) {
120
+ clearTimeout(_autoContinueTimer.current);
121
+ _autoContinueTimer.current = null;
122
+ }
123
+ };
124
+ """
125
+ if "_cancelAutoContinue.current = ()" not in c:
126
+ # Insert before return
127
+ c = c.replace(
128
+ "\n return {\n messages: chat.messages,",
129
+ cancel_func + "\n return {\n messages: chat.messages,"
130
+ )
131
+
132
+ # 7. Return values
133
+ if "_showAutoContinue," not in c:
134
+ c = c.replace(
135
+ "refreshMessages,\n };",
136
+ "refreshMessages,\n _showAutoContinue,\n _cancelAutoContinue,\n };"
137
+ )
138
+
139
+ with open(USE_AGENT_CHAT, "w") as f: f.write(c)
140
+ print("OK: useAgentChat.ts")
141
+
142
+
143
+ def patch_chat_input():
144
+ with open(CHAT_INPUT, "r") as f:
145
+ c = f.read()
146
+
147
+ # Fix DatasetUploadResponse
148
+ if "DatasetUploadResponse" in c and "inline" not in c:
149
+ wrong = "import type { DatasetUploadResponse } from '@/types/agent';"
150
+ if wrong in c:
151
+ c = c.replace(wrong, "")
152
+ c = c.replace(
153
+ "import { apiFetch } from '@/utils/api';",
154
+ "import { apiFetch } from '@/utils/api';\ntype DatasetUploadResponse = { url: string; repo_id: string; filename: string; size: number; };"
155
+ )
156
+
157
+ # Add Button import
158
+ if "Button" not in c:
159
+ c = c.replace(
160
+ "import { Box, IconButton, Stack, Tooltip } from '@mui/material';",
161
+ "import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';"
162
+ )
163
+
164
+ # Add props to interface
165
+ if "_showAutoContinue" not in c:
166
+ match = re.search(r'interface ChatInputProps\s*\{[^}]*\}', c, re.DOTALL)
167
+ if match:
168
+ new_iface = """interface ChatInputProps {
169
+ sessionId: string;
170
+ initialModelPath: string | null | undefined;
171
+ onSend: (text: string) => Promise<void>;
172
+ onStop: () => void;
173
+ onDatasetUploaded: () => Promise<boolean>;
174
+ isProcessing: boolean;
175
+ disabled: boolean;
176
+ placeholder?: string;
177
+ _showAutoContinue?: boolean;
178
+ _cancelAutoContinue?: React.RefObject<() => void>;
179
+ }"""
180
+ c = c.replace(match.group(0), new_iface)
181
+
182
+ # Add pause button
183
+ btn = """
184
+ {/* Pause auto-continue for free models */}
185
+ {_showAutoContinue && _cancelAutoContinue?.current && (
186
+ <Stack direction="row" spacing={1} sx={{ mb: 1, px: 1 }}>
187
+ <Button
188
+ variant="outlined"
189
+ color="secondary"
190
+ size="small"
191
+ onClick={_cancelAutoContinue.current}
192
+ sx={{ textTransform: 'none', fontSize: '0.75rem' }}
193
+ >
194
+ Tạm dừng (để nhập nội dung khác)
195
+ </Button>
196
+ </Stack>
197
+ )}
198
+ """
199
+ if "Tạm dừng" not in c:
200
+ c = c.replace("<Box sx={{ flex: 1 ", btn + "\n <Box sx={{ flex: 1 ")
201
+
202
+ with open(CHAT_INPUT, "w") as f: f.write(c)
203
+ print("OK: ChatInput.tsx")
204
+
205
+
206
+ def patch_session_chat():
207
+ with open(SESSION_CHAT, "r") as f:
208
+ c = f.read()
209
+
210
+ # Find the ChatInput usage and add new props
211
+ if "_showAutoContinue" not in c:
212
+ chat_input_pattern = r'(<ChatInput[^>]*sessionId={sessionId}[^>]*initialModelPath={initialModelPath}[^>]*)(\s*/>)'
213
+ new_props = r'\1 _showAutoContinue={_showAutoContinue} _cancelAutoContinue={_cancelAutoContinue}\2'
214
+ c = re.sub(chat_input_pattern, new_props, c)
215
+
216
+ with open(SESSION_CHAT, "w") as f: f.write(c)
217
+ print("OK: SessionChat.tsx")
218
+
219
+
220
+ def patch_agent_loop():
221
+ agent_loop = "/app/agent/core/agent_loop.py"
222
+ if not os.path.exists(agent_loop):
223
+ print(f"SKIP: {agent_loop} not found (backend)")
224
+ return
225
+
226
+ with open(agent_loop, "r") as f:
227
+ c = f.read()
228
+
229
+ if "_check_task_incomplete" not in c:
230
+ func = '''
231
+
232
+ def _unfinished_plan_items(session: Session) -> list[dict[str, str]]:
233
+ plan = getattr(session, "current_plan", None) or []
234
+ return [item for item in plan if item.get("status") in ("pending", "in_progress")]
235
+
236
+ '''
237
+ c = c.replace("class Handlers:", func + "\n\nclass Handlers:")
238
+
239
+ if "task_incomplete" not in c:
240
+ check = '''
241
+ # === Auto-continue: if model stopped but plan incomplete ===
242
+ if not llm_result.tool_calls_acc and llm_result.content:
243
+ unfinished = _unfinished_plan_items(session)
244
+ if unfinished:
245
+ await session.send_event(
246
+ Event(event_type="task_incomplete", data={"incomplete_plan": unfinished})
247
+ )
248
+ '''
249
+ marker = " # -- End of turn --"
250
+ if marker in c:
251
+ c = c.replace(marker, check + "\n" + marker)
252
+
253
+ with open(agent_loop, "w") as f: f.write(c)
254
+ print("OK: agent_loop.py")
255
+
256
+
257
+ if __name__ == "__main__":
258
+ patch_events()
259
+ patch_sse()
260
+ patch_use_agent_chat()
261
+ patch_chat_input()
262
+ patch_session_chat()
263
+ patch_agent_loop()
264
+ print("\nDONE: V4 - everything applied")