Auto-continue feature for free models

#2
by bep40 - opened
Dockerfile CHANGED
@@ -14,6 +14,9 @@ RUN python3 /tmp/patch_use_agent_chat_full.py
14
  COPY patch_chat_input.py /tmp/patch_chat_input.py
15
  RUN python3 /tmp/patch_chat_input.py
16
 
 
 
 
17
  WORKDIR /source/frontend
18
  RUN npm config set fetch-timeout 120000 && \
19
  npm config set fetch-retries 3 && \
@@ -43,8 +46,8 @@ COPY configs/frontend_agent_config.json ./configs/frontend_agent_config.json
43
  COPY patch_models.py /tmp/patch_models.py
44
  RUN python /tmp/patch_models.py
45
 
46
- COPY patch_auto_continue.py /tmp/patch_auto_continue.py
47
- RUN python /tmp/patch_auto_continue.py
48
 
49
  RUN mkdir -p /app/session_logs && chown -R user:user /app
50
 
@@ -56,4 +59,5 @@ ENV HOME=/home/user \
56
 
57
  EXPOSE 7860
58
  WORKDIR /app/backend
 
59
  CMD ["sh", "start.sh"]
 
14
  COPY patch_chat_input.py /tmp/patch_chat_input.py
15
  RUN python3 /tmp/patch_chat_input.py
16
 
17
+ COPY patch_sse_transport.py /tmp/patch_sse_transport.py
18
+ RUN python3 /tmp/patch_sse_transport.py
19
+
20
  WORKDIR /source/frontend
21
  RUN npm config set fetch-timeout 120000 && \
22
  npm config set fetch-retries 3 && \
 
46
  COPY patch_models.py /tmp/patch_models.py
47
  RUN python /tmp/patch_models.py
48
 
49
+ COPY patch_agent_loop.py /tmp/patch_agent_loop.py
50
+ RUN python /tmp/patch_agent_loop.py || echo "agent_loop patch not applied (may need adjustment)"
51
 
52
  RUN mkdir -p /app/session_logs && chown -R user:user /app
53
 
 
59
 
60
  EXPOSE 7860
61
  WORKDIR /app/backend
62
+ # python:slim does not include bash by default; start.sh is POSIX-compatible.
63
  CMD ["sh", "start.sh"]
patch_agent_loop.py CHANGED
@@ -11,28 +11,18 @@ def patch():
11
  with open(AGENT_LOOP, "r", encoding="utf-8") as f:
12
  content = f.read()
13
 
14
- # Add import re if needed for detection
15
  # Add _check_task_incomplete function before class Handlers
16
-
17
  check_func = '''
18
 
19
  def _check_task_incomplete(session: Session, llm_result: LLMResult) -> bool:
20
- """Check if model stopped streaming but task is incomplete.
21
-
22
- Returns True when:
23
- - LLM finished without tool calls
24
- - But there are unfinished plan items
25
- """
26
  if llm_result.tool_calls_acc:
27
  return False # Has tool calls, task continues via tool execution
28
 
29
  plan = getattr(session, "current_plan", None) or []
30
  unfinished = [item for item in plan if item.get("status") in ("pending", "in_progress")]
31
 
32
- # Also check if there are any pending actions in the loop
33
- pending_actions = getattr(session, "pending_actions", None)
34
-
35
- return len(unfinished) > 0 or bool(pending_actions)
36
 
37
 
38
  def _unfinished_plan_items(session: Session) -> list[dict[str, str]]:
@@ -42,19 +32,16 @@ def _unfinished_plan_items(session: Session) -> list[dict[str, str]]:
42
 
43
  '''
44
 
45
- # Insert after LLMResult dataclass definition and before Handlers class
46
  if "_check_task_incomplete" not in content:
47
  content = content.replace(
48
  "class Handlers:",
49
  check_func + "\n\nclass Handlers:"
50
  )
51
 
52
- # Now we need to add the call in the run_agent method
53
- # Find where LLM result is processed and check for incomplete task
54
  check_call = '''
55
  # === Check for incomplete task after LLM response ===
56
  if not llm_result.tool_calls_acc and llm_result.content:
57
- # Model finished without tool calls - check if task is done
58
  unfinished = _unfinished_plan_items(session)
59
  if unfinished:
60
  await session.send_event(
@@ -68,24 +55,16 @@ def _unfinished_plan_items(session: Session) -> list[dict[str, str]]:
68
  )
69
  '''
70
 
71
- # Insert this check after processing tool calls but before final_response check
72
- # Look for pattern: iteration increment and session updates
73
  if "task_incomplete" not in content:
74
- # Find location after tool_calls processing
75
  insert_marker = " # -- End of turn --"
76
  if insert_marker in content:
77
- content = content.replace(
78
- insert_marker,
79
- check_call + "\n" + insert_marker
80
- )
81
  else:
82
- # Try another location - after llm_result processing
83
  result_marker = "final_response = llm_result.content or None"
84
  if result_marker in content:
85
- content = content.replace(
86
- result_marker,
87
- result_marker + "\n" + check_call
88
- )
89
 
90
  with open(AGENT_LOOP, "w", encoding="utf-8") as f:
91
  f.write(content)
 
11
  with open(AGENT_LOOP, "r", encoding="utf-8") as f:
12
  content = f.read()
13
 
 
14
  # Add _check_task_incomplete function before class Handlers
 
15
  check_func = '''
16
 
17
  def _check_task_incomplete(session: Session, llm_result: LLMResult) -> bool:
18
+ """Check if model stopped streaming but task is incomplete."""
 
 
 
 
 
19
  if llm_result.tool_calls_acc:
20
  return False # Has tool calls, task continues via tool execution
21
 
22
  plan = getattr(session, "current_plan", None) or []
23
  unfinished = [item for item in plan if item.get("status") in ("pending", "in_progress")]
24
 
25
+ return len(unfinished) > 0
 
 
 
26
 
27
 
28
  def _unfinished_plan_items(session: Session) -> list[dict[str, str]]:
 
32
 
33
  '''
34
 
 
35
  if "_check_task_incomplete" not in content:
36
  content = content.replace(
37
  "class Handlers:",
38
  check_func + "\n\nclass Handlers:"
39
  )
40
 
41
+ # Add the check after LLM response processing
 
42
  check_call = '''
43
  # === Check for incomplete task after LLM response ===
44
  if not llm_result.tool_calls_acc and llm_result.content:
 
45
  unfinished = _unfinished_plan_items(session)
46
  if unfinished:
47
  await session.send_event(
 
55
  )
56
  '''
57
 
 
 
58
  if "task_incomplete" not in content:
59
+ # Find a good insertion point - after tool calls processing
60
  insert_marker = " # -- End of turn --"
61
  if insert_marker in content:
62
+ content = content.replace(insert_marker, check_call + "\n" + insert_marker)
 
 
 
63
  else:
64
+ # Try another location
65
  result_marker = "final_response = llm_result.content or None"
66
  if result_marker in content:
67
+ content = content.replace(result_marker, result_marker + "\n" + check_call)
 
 
 
68
 
69
  with open(AGENT_LOOP, "w", encoding="utf-8") as f:
70
  f.write(content)
patch_chat_input.py CHANGED
@@ -1,10 +1,9 @@
1
- """Patch ChatInput.tsx - Fixed version with all required props."""
2
 
3
  CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
4
 
5
  def patch():
6
- import os
7
- import re
8
  if not os.path.exists(CHAT_INPUT):
9
  print(f"SKIP: {CHAT_INPUT} not found")
10
  return
@@ -12,22 +11,24 @@ def patch():
12
  with open(CHAT_INPUT, "r", encoding="utf-8") as f:
13
  content = f.read()
14
 
15
- # Add Button to MUI imports
16
- if "Button" not in content:
17
  content = content.replace(
18
  "import { Box, IconButton, Stack, Tooltip } from '@mui/material';",
19
  "import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';"
20
  )
21
 
22
- # Add import for DatasetUploadResponse if needed
23
  if "DatasetUploadResponse" not in content:
24
  content = content.replace(
25
  "import { apiFetch } from '@/utils/api';",
26
  "import { apiFetch } from '@/utils/api';\nimport type { DatasetUploadResponse } from '@/types/agent';"
27
  )
28
 
29
- # Replace interface ChatInputProps with complete version including auto-continue props
30
- new_props = '''interface ChatInputProps {
 
 
31
  sessionId: string;
32
  initialModelPath: string | null | undefined;
33
  onSend: (text: string) => Promise<void>;
@@ -40,19 +41,21 @@ def patch():
40
  taskIncompleteInfo?: { incompletePlan: Array<{ id: string; content: string; status: string }> } | null;
41
  startAutoContinue?: () => void;
42
  cancelAutoContinue?: () => void;
43
- }'''
44
-
45
- if "showAutoContinue" not in content:
46
- # Find existing interface
47
- match = re.search(r'interface ChatInputProps\s*\{[^}]*\}', content, re.DOTALL)
48
  if match:
49
- content = content.replace(match.group(0), new_props)
50
- print("OK: Replaced ChatInputProps interface")
51
  else:
52
- print("WARN: Could not find ChatInputProps interface")
 
 
 
53
 
54
- # Add auto-continue buttons before the flex:1 Box
55
- buttons_code = ''' {/* Auto-continue controls for free models */}
 
56
  {showAutoContinue && taskIncompleteInfo && (
57
  <Stack direction="row" spacing={1} sx={{ mb: 1, px: 1 }}>
58
  <Button
@@ -74,15 +77,15 @@ def patch():
74
  Tạm dừng
75
  </Button>
76
  </Stack>
77
- )}'''
 
78
 
79
  if "Tự động tiếp tục" not in content:
80
  # Insert before <Box sx={{ flex: 1 }}
81
  content = content.replace(
82
- "<Box sx={{ flex: 1 }} />",
83
- buttons_code + "\n <Box sx={{ flex: 1 }} />"
84
  )
85
- print("OK: Added auto-continue buttons")
86
 
87
  with open(CHAT_INPUT, "w", encoding="utf-8") as f:
88
  f.write(content)
 
1
+ """Patch ChatInput.tsx for auto-continue buttons."""
2
 
3
  CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
4
 
5
  def patch():
6
+ import os, re
 
7
  if not os.path.exists(CHAT_INPUT):
8
  print(f"SKIP: {CHAT_INPUT} not found")
9
  return
 
11
  with open(CHAT_INPUT, "r", encoding="utf-8") as f:
12
  content = f.read()
13
 
14
+ # 1. Add Button to MUI imports
15
+ if "Button" not in content and "@mui/material" in content:
16
  content = content.replace(
17
  "import { Box, IconButton, Stack, Tooltip } from '@mui/material';",
18
  "import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';"
19
  )
20
 
21
+ # 2. Add DatasetUploadResponse import if needed
22
  if "DatasetUploadResponse" not in content:
23
  content = content.replace(
24
  "import { apiFetch } from '@/utils/api';",
25
  "import { apiFetch } from '@/utils/api';\nimport type { DatasetUploadResponse } from '@/types/agent';"
26
  )
27
 
28
+ # 3. Add props to interface
29
+ if "showAutoContinue" not in content:
30
+ props_code = '''
31
+ interface ChatInputProps {
32
  sessionId: string;
33
  initialModelPath: string | null | undefined;
34
  onSend: (text: string) => Promise<void>;
 
41
  taskIncompleteInfo?: { incompletePlan: Array<{ id: string; content: string; status: string }> } | null;
42
  startAutoContinue?: () => void;
43
  cancelAutoContinue?: () => void;
44
+ }
45
+ '''
46
+ # Find and replace existing interface
47
+ match = re.search(r'interface ChatInputProps[^}]*[}][^}]*[}]', content, re.DOTALL)
 
48
  if match:
49
+ content = content.replace(match.group(0), props_code)
 
50
  else:
51
+ content = content.replace(
52
+ "export default function ChatInput",
53
+ props_code + "\nexport default function ChatInput"
54
+ )
55
 
56
+ # 4. Add auto-continue buttons before the flex box
57
+ buttons_code = '''
58
+ {/* Auto-continue controls for free models */}
59
  {showAutoContinue && taskIncompleteInfo && (
60
  <Stack direction="row" spacing={1} sx={{ mb: 1, px: 1 }}>
61
  <Button
 
77
  Tạm dừng
78
  </Button>
79
  </Stack>
80
+ )}
81
+ '''
82
 
83
  if "Tự động tiếp tục" not in content:
84
  # Insert before <Box sx={{ flex: 1 }}
85
  content = content.replace(
86
+ "<Box sx={{ flex: 1 ",
87
+ buttons_code + " <Box sx={{ flex: 1 "
88
  )
 
89
 
90
  with open(CHAT_INPUT, "w", encoding="utf-8") as f:
91
  f.write(content)
patch_sse_transport.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Patch sse-chat-transport.ts - handle task_incomplete event."""
2
+
3
+ SSE_TRANSPORT = "/source/frontend/src/lib/sse-chat-transport.ts"
4
+
5
+ def patch():
6
+ import os
7
+ if not os.path.exists(SSE_TRANSPORT):
8
+ print(f"SKIP: {SSE_TRANSPORT} not found")
9
+ return
10
+
11
+ with open(SSE_TRANSPORT, "r", encoding="utf-8") as f:
12
+ content = f.read()
13
+
14
+ # Add task_incomplete case in createEventToChunkStream
15
+ if "case 'task_incomplete'" not in content:
16
+ task_incomplete_case = ''' case 'task_incomplete':
17
+ sideChannel.onTaskIncomplete(
18
+ (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
19
+ );
20
+ break;
21
+ '''
22
+ content = content.replace(
23
+ "case 'turn_complete':",
24
+ task_incomplete_case + "\n case 'turn_complete':"
25
+ )
26
+
27
+ with open(SSE_TRANSPORT, "w", encoding="utf-8") as f:
28
+ f.write(content)
29
+ print(f"OK: Patched {SSE_TRANSPORT}")
30
+
31
+ if __name__ == "__main__":
32
+ patch()
patch_use_agent_chat_full.py CHANGED
@@ -1,4 +1,4 @@
1
- """Complete patch for useAgentChat.ts - auto-continue feature (fixed TS)."""
2
 
3
  USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
4
 
@@ -25,12 +25,13 @@ def patch():
25
  const [showAutoContinue, setShowAutoContinue] = useState(false);
26
  const [taskIncompleteInfo, setTaskIncompleteInfo] = useState<{
27
  incompletePlan: Array<{ id: string; content: string; status: string }>;
28
- } | null>(null);'''
 
29
 
30
  if "autoContinueTimerRef" not in content:
31
  content = content.replace(
32
  "callbacksRef.current = { onReady, onError, onSessionDead };",
33
- "callbacksRef.current = { onReady, onError, onSessionDead };\n" + state_decl
34
  )
35
 
36
  # 3. Add onTaskIncomplete to SideChannelCallbacks interface
@@ -41,7 +42,7 @@ def patch():
41
  )
42
 
43
  # 4. Add onTaskIncomplete handler in sideChannel
44
- sidechannel_handler = ''' onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => {
45
  setTaskIncompleteInfo({ incompletePlan });
46
  setShowAutoContinue(true);
47
  // Auto-start 10s timer for free models
@@ -53,7 +54,7 @@ def patch():
53
  }, 10000);
54
  },'''
55
 
56
- if "onTaskIncomplete: (incompletePlan:" not in content:
57
  content = content.replace(
58
  " onInterrupted: () => { /* no-op — handled by stop() caller */ },",
59
  sidechannel_handler + "\n onInterrupted: () => { /* no-op — handled by stop() caller */ },"
@@ -96,7 +97,8 @@ def patch():
96
  clearTimeout(autoContinueTimerRef.current);
97
  }
98
  };
99
- }, []);'''
 
100
 
101
  if "startAutoContinue" not in content:
102
  content = content.replace(
 
1
+ """Patch useAgentChat.ts - auto-continue feature."""
2
 
3
  USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
4
 
 
25
  const [showAutoContinue, setShowAutoContinue] = useState(false);
26
  const [taskIncompleteInfo, setTaskIncompleteInfo] = useState<{
27
  incompletePlan: Array<{ id: string; content: string; status: string }>;
28
+ } | null>(null);
29
+ '''
30
 
31
  if "autoContinueTimerRef" not in content:
32
  content = content.replace(
33
  "callbacksRef.current = { onReady, onError, onSessionDead };",
34
+ "callbacksRef.current = { onReady, onError, onSessionDead };" + state_decl
35
  )
36
 
37
  # 3. Add onTaskIncomplete to SideChannelCallbacks interface
 
42
  )
43
 
44
  # 4. Add onTaskIncomplete handler in sideChannel
45
+ sidechannel_handler = ''' onTaskIncomplete: (incompletePlan) => {
46
  setTaskIncompleteInfo({ incompletePlan });
47
  setShowAutoContinue(true);
48
  // Auto-start 10s timer for free models
 
54
  }, 10000);
55
  },'''
56
 
57
+ if "onTaskIncomplete: (incompletePlan)" not in content:
58
  content = content.replace(
59
  " onInterrupted: () => { /* no-op — handled by stop() caller */ },",
60
  sidechannel_handler + "\n onInterrupted: () => { /* no-op — handled by stop() caller */ },"
 
97
  clearTimeout(autoContinueTimerRef.current);
98
  }
99
  };
100
+ }, []);
101
+ '''
102
 
103
  if "startAutoContinue" not in content:
104
  content = content.replace(