diff --git a/scripts/llm_client.py b/scripts/llm_client.py index f898b2f..fc3a2ee 100644 --- a/scripts/llm_client.py +++ b/scripts/llm_client.py @@ -104,6 +104,7 @@ class LLMClient: # Parse delta dari chunk delta = chunk.get('choices', [{}])[0].get('delta', {}) + finish_reason = chunk.get('choices', [{}])[0].get('finish_reason', None) # Stream reasoning content jika ada if 'reasoning_content' in delta: @@ -147,7 +148,32 @@ class LLMClient: message = {'content': full_content} if full_tool_calls: - message['tool_calls'] = full_tool_calls + # Filter tool_calls yang valid (ada name dan arguments) + valid_tool_calls = [] + for tc in full_tool_calls: + name = tc.get('function', {}).get('name') + args_str = tc.get('function', {}).get('arguments') + tc_id = tc.get('id') + + # Pastikan name dan arguments ada + if name and args_str and args_str.strip(): + # Generate ID jika kosong + if not tc_id: + tc_id = f"call_{len(valid_tool_calls)}" + tc['id'] = tc_id + + try: + # Validate dan re-encode JSON untuk format yang konsisten + parsed_args = json.loads(args_str) + tc['function']['arguments'] = json.dumps(parsed_args, ensure_ascii=False) + valid_tool_calls.append(tc) + except json.JSONDecodeError: + # Invalid JSON, coba raw string tapi hanya jika tidak kosong + tc['function']['arguments'] = args_str + valid_tool_calls.append(tc) + + if valid_tool_calls: + message['tool_calls'] = valid_tool_calls response = {'choices': [{'message': message}]} except urllib.error.HTTPError as e: diff --git a/tui/agent.py b/tui/agent.py index f8e3d10..72ccb31 100644 --- a/tui/agent.py +++ b/tui/agent.py @@ -87,18 +87,26 @@ def _agent_loop(app): log(app, "system", f" step {step + 1} \u2014 Thinking...") app.scroll = 999999 - # Streaming response - buat placeholder untuk AI response - stream_idx = len(app.log) - log(app, "ai", "...") # Placeholder sambil streaming - + # Streaming response stream_buffer = [] + placeholder_marker = None + def on_stream_chunk(chunk): + nonlocal placeholder_marker + # Buat placeholder di chunk pertama saja + if placeholder_marker is None: + placeholder_marker = f"_stream_placeholder_{step}" + log(app, "ai", placeholder_marker) stream_buffer.append(chunk) current_text = ''.join(stream_buffer) # Update placeholder secara real-time - if stream_idx < len(app.log): - app.log[stream_idx]['text'] = current_text - app.scroll = 999999 + for i in range(len(app.log) - 1, -1, -1): + # Cari placeholder berdasarkan content aslinya (atau apa yang sudah diupdate) + # Karena placeholder text berubah seiring streaming, kita harus teliti + if app.log[i].get('role') == 'ai' and (app.log[i].get('text') == placeholder_marker or (i > 0 and app.log[i-1].get('role') == 'system' and 'Thinking' in app.log[i-1].get('text', ''))): + app.log[i]['text'] = current_text + break + app.scroll = 999999 response = app.llm.chat(app.messages, tools=app.TOOLS, on_stream_chunk=on_stream_chunk) @@ -111,11 +119,23 @@ def _agent_loop(app): if response.warning: log(app, "system", f" {response.warning}") - if response.tool_calls: + # Cek apakah ada tool_calls + has_tool_calls = bool(response.tool_calls) + + if has_tool_calls: + # Hapus placeholder jika ada tool_calls (cari semua AI log yang mungkin placeholder) + if placeholder_marker is not None: + for i in range(len(app.log) - 1, -1, -1): + # Jika ini adalah log AI yang muncul tepat setelah "Thinking..." atau contains placeholder marker + if app.log[i].get('role') == 'ai': + text = app.log[i].get('text', '') + # Hapus jika text-nya adalah placeholder atau jika itu adalah entry AI yang baru saja kita buat untuk streaming + if placeholder_marker in text or text == "" or text == "...": + app.log.pop(i) + _add_msg(app, "assistant", response.content, tool_calls=response.tool_calls) - # Placeholder sudah terupdate via streaming, jangan log lagi - if stream_idx < len(app.log) and response.content and response.content.strip(): - app.log[stream_idx]['text'] = response.content + + # Log tool_calls dengan label AI for tc in response.tool_calls: tname = tc["function"]["name"] targs = tc["function"]["arguments"] @@ -125,6 +145,10 @@ def _agent_loop(app): })) app.scroll = 999999 execute_tool(app, tc) + + # Log content AI setelah tools (jika ada) + if response.content and response.content.strip(): + log(app, "ai", response.content) else: if response.content: _add_msg(app, "assistant", response.content)