moelove commited on
Commit
8e089e0
·
unverified ·
1 Parent(s): 983cce0

show thinking

Browse files

Signed-off-by: Jintao Zhang <zhangjintao9020@gmail.com>

Files changed (2) hide show
  1. src/amcp/acp_agent.py +20 -24
  2. src/amcp/llm.py +32 -1
src/amcp/acp_agent.py CHANGED
@@ -29,6 +29,7 @@ from acp import (
29
  start_tool_call,
30
  text_block,
31
  update_agent_message,
 
32
  update_tool_call,
33
  )
34
  from acp.interfaces import Client
@@ -55,8 +56,8 @@ from acp.schema import (
55
  )
56
 
57
  from .agent_spec import ResolvedAgentSpec, get_default_agent_spec
58
- from .chat import _make_client, _resolve_api_key, _resolve_base_url
59
  from .config import load_config
 
60
  from .mcp_client import call_mcp_tool, list_mcp_tools
61
  from .tools import ToolResult, get_tool_registry
62
 
@@ -473,10 +474,7 @@ class AMCPAgent(Agent):
473
  async def _process_prompt(self, session: ACPSession, user_text: str) -> str:
474
  """Process prompt with LLM and tools."""
475
  cfg = load_config()
476
- base_url = _resolve_base_url(self.agent_spec.base_url or None, cfg.chat)
477
- api_key = _resolve_api_key(None, cfg.chat)
478
- client = _make_client(base_url, api_key)
479
- model = cfg.chat.model if cfg.chat else "DeepSeek-V3.1-Terminus"
480
 
481
  system_prompt = self._get_system_prompt(session)
482
  messages = [{"role": "system", "content": system_prompt}]
@@ -490,19 +488,17 @@ class AMCPAgent(Agent):
490
  if session.session_id in self._cancelled_sessions:
491
  raise asyncio.CancelledError()
492
 
493
- resp = client.chat.completions.create(
494
- model=model,
495
- messages=messages,
496
- tools=tools if tools else None,
497
- tool_choice="auto" if tools else None,
498
- stream=False,
499
- )
500
 
501
- msg = resp.choices[0].message
502
- tool_calls = getattr(msg, "tool_calls", None)
 
 
 
 
503
 
504
- if not tool_calls:
505
- final_text = msg.content or ""
506
  if self._conn:
507
  await self._conn.session_update(
508
  session_id=session.session_id,
@@ -510,9 +506,9 @@ class AMCPAgent(Agent):
510
  )
511
  return final_text
512
 
513
- for tc in tool_calls:
514
- tool_name = tc.function.name
515
- args = json.loads(tc.function.arguments or "{}")
516
  tool_call_id = f"call_{uuid4().hex[:8]}"
517
 
518
  # Request permission in "ask" mode for write operations
@@ -522,7 +518,7 @@ class AMCPAgent(Agent):
522
  messages.append(
523
  {
524
  "role": "tool",
525
- "tool_call_id": tc.id,
526
  "name": tool_name,
527
  "content": "Permission denied by user",
528
  }
@@ -555,17 +551,17 @@ class AMCPAgent(Agent):
555
  messages.append(
556
  {
557
  "role": "assistant",
558
- "content": msg.content or "",
559
  "tool_calls": [
560
  {
561
- "id": tc.id,
562
  "type": "function",
563
- "function": {"name": tool_name, "arguments": tc.function.arguments or "{}"},
564
  }
565
  ],
566
  }
567
  )
568
- messages.append({"role": "tool", "tool_call_id": tc.id, "name": tool_name, "content": result[:8000]})
569
 
570
  return "Maximum steps reached. Please try a simpler request."
571
 
 
29
  start_tool_call,
30
  text_block,
31
  update_agent_message,
32
+ update_agent_thought,
33
  update_tool_call,
34
  )
35
  from acp.interfaces import Client
 
56
  )
57
 
58
  from .agent_spec import ResolvedAgentSpec, get_default_agent_spec
 
59
  from .config import load_config
60
+ from .llm import create_llm_client
61
  from .mcp_client import call_mcp_tool, list_mcp_tools
62
  from .tools import ToolResult, get_tool_registry
63
 
 
474
  async def _process_prompt(self, session: ACPSession, user_text: str) -> str:
475
  """Process prompt with LLM and tools."""
476
  cfg = load_config()
477
+ llm_client = create_llm_client(cfg.chat)
 
 
 
478
 
479
  system_prompt = self._get_system_prompt(session)
480
  messages = [{"role": "system", "content": system_prompt}]
 
488
  if session.session_id in self._cancelled_sessions:
489
  raise asyncio.CancelledError()
490
 
491
+ resp = llm_client.chat(messages=messages, tools=tools if tools else None)
 
 
 
 
 
 
492
 
493
+ # Send thinking content if available
494
+ if resp.thinking and self._conn:
495
+ await self._conn.session_update(
496
+ session_id=session.session_id,
497
+ update=update_agent_thought(text_block(resp.thinking)),
498
+ )
499
 
500
+ if not resp.tool_calls:
501
+ final_text = resp.content or ""
502
  if self._conn:
503
  await self._conn.session_update(
504
  session_id=session.session_id,
 
506
  )
507
  return final_text
508
 
509
+ for tc in resp.tool_calls:
510
+ tool_name = tc["name"]
511
+ args = json.loads(tc["arguments"] or "{}")
512
  tool_call_id = f"call_{uuid4().hex[:8]}"
513
 
514
  # Request permission in "ask" mode for write operations
 
518
  messages.append(
519
  {
520
  "role": "tool",
521
+ "tool_call_id": tc["id"],
522
  "name": tool_name,
523
  "content": "Permission denied by user",
524
  }
 
551
  messages.append(
552
  {
553
  "role": "assistant",
554
+ "content": resp.content or "",
555
  "tool_calls": [
556
  {
557
+ "id": tc["id"],
558
  "type": "function",
559
+ "function": {"name": tool_name, "arguments": tc["arguments"] or "{}"},
560
  }
561
  ],
562
  }
563
  )
564
+ messages.append({"role": "tool", "tool_call_id": tc["id"], "name": tool_name, "content": result[:8000]})
565
 
566
  return "Maximum steps reached. Please try a simpler request."
567
 
src/amcp/llm.py CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
4
 
5
  import json
6
  import os
 
7
  from abc import ABC, abstractmethod
8
  from dataclasses import dataclass
9
  from typing import Any
@@ -11,6 +12,17 @@ from typing import Any
11
  from .config import ChatConfig
12
 
13
 
 
 
 
 
 
 
 
 
 
 
 
14
  @dataclass
15
  class LLMResponse:
16
  """Unified response from LLM."""
@@ -18,6 +30,7 @@ class LLMResponse:
18
  content: str | None
19
  tool_calls: list[dict[str, Any]] | None = None
20
  stop_reason: str | None = None
 
21
 
22
 
23
  class BaseLLMClient(ABC):
@@ -53,7 +66,20 @@ class OpenAIClient(BaseLLMClient):
53
  {"id": tc.id, "name": tc.function.name, "arguments": tc.function.arguments} for tc in msg.tool_calls
54
  ]
55
 
56
- return LLMResponse(content=msg.content, tool_calls=tool_calls, stop_reason=resp.choices[0].finish_reason)
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
 
59
  class OpenAIResponsesClient(BaseLLMClient):
@@ -181,10 +207,14 @@ class AnthropicClient(BaseLLMClient):
181
 
182
  content_parts = []
183
  tool_calls = []
 
184
 
185
  for block in resp.content:
186
  if block.type == "text":
187
  content_parts.append(block.text)
 
 
 
188
  elif block.type == "tool_use":
189
  tool_calls.append({"id": block.id, "name": block.name, "arguments": json.dumps(block.input)})
190
 
@@ -192,6 +222,7 @@ class AnthropicClient(BaseLLMClient):
192
  content="\n".join(content_parts) if content_parts else None,
193
  tool_calls=tool_calls if tool_calls else None,
194
  stop_reason=resp.stop_reason,
 
195
  )
196
 
197
 
 
4
 
5
  import json
6
  import os
7
+ import re
8
  from abc import ABC, abstractmethod
9
  from dataclasses import dataclass
10
  from typing import Any
 
12
  from .config import ChatConfig
13
 
14
 
15
+ def _extract_think_tags(content: str) -> tuple[str | None, str]:
16
+ """Extract content from <think> tags and return (thinking, remaining_content)."""
17
+ pattern = r"<think>(.*?)</think>"
18
+ matches = re.findall(pattern, content, re.DOTALL)
19
+ if matches:
20
+ thinking = "\n".join(m.strip() for m in matches)
21
+ remaining = re.sub(pattern, "", content, flags=re.DOTALL).strip()
22
+ return thinking, remaining
23
+ return None, content
24
+
25
+
26
  @dataclass
27
  class LLMResponse:
28
  """Unified response from LLM."""
 
30
  content: str | None
31
  tool_calls: list[dict[str, Any]] | None = None
32
  stop_reason: str | None = None
33
+ thinking: str | None = None # Reasoning/thinking content from LLM
34
 
35
 
36
  class BaseLLMClient(ABC):
 
66
  {"id": tc.id, "name": tc.function.name, "arguments": tc.function.arguments} for tc in msg.tool_calls
67
  ]
68
 
69
+ # Extract thinking content
70
+ thinking = None
71
+ content = msg.content
72
+
73
+ # Check for reasoning_content field (DeepSeek, some OpenAI-compatible APIs)
74
+ if hasattr(msg, "reasoning_content") and msg.reasoning_content:
75
+ thinking = msg.reasoning_content
76
+ # Check for <think> tags in content
77
+ elif content:
78
+ thinking, content = _extract_think_tags(content)
79
+
80
+ return LLMResponse(
81
+ content=content, tool_calls=tool_calls, stop_reason=resp.choices[0].finish_reason, thinking=thinking
82
+ )
83
 
84
 
85
  class OpenAIResponsesClient(BaseLLMClient):
 
207
 
208
  content_parts = []
209
  tool_calls = []
210
+ thinking_parts = []
211
 
212
  for block in resp.content:
213
  if block.type == "text":
214
  content_parts.append(block.text)
215
+ elif block.type == "thinking":
216
+ # Anthropic extended thinking block
217
+ thinking_parts.append(getattr(block, "thinking", ""))
218
  elif block.type == "tool_use":
219
  tool_calls.append({"id": block.id, "name": block.name, "arguments": json.dumps(block.input)})
220
 
 
222
  content="\n".join(content_parts) if content_parts else None,
223
  tool_calls=tool_calls if tool_calls else None,
224
  stop_reason=resp.stop_reason,
225
+ thinking="\n".join(thinking_parts) if thinking_parts else None,
226
  )
227
 
228