Spaces:
Configuration error
Configuration error
show thinking
Browse filesSigned-off-by: Jintao Zhang <zhangjintao9020@gmail.com>
- src/amcp/acp_agent.py +20 -24
- 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 |
-
|
| 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 =
|
| 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 |
-
|
| 502 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
|
| 504 |
-
if not tool_calls:
|
| 505 |
-
final_text =
|
| 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
|
| 515 |
-
args = json.loads(tc
|
| 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
|
| 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":
|
| 559 |
"tool_calls": [
|
| 560 |
{
|
| 561 |
-
"id": tc
|
| 562 |
"type": "function",
|
| 563 |
-
"function": {"name": tool_name, "arguments": tc
|
| 564 |
}
|
| 565 |
],
|
| 566 |
}
|
| 567 |
)
|
| 568 |
-
messages.append({"role": "tool", "tool_call_id": tc
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|