add NativeClaudeSession+NativeToolClient, support parallel tool calls, fix tool_calls[-1:], add llm log

This commit is contained in:
Liang Jiaqing
2026-03-21 12:13:46 +08:00
parent 85f17db487
commit fdc24e4dd0
5 changed files with 207 additions and 87 deletions

View File

@@ -17,9 +17,10 @@ class BaseHandler:
def tool_before_callback(self, tool_name, args, response): pass def tool_before_callback(self, tool_name, args, response): pass
def tool_after_callback(self, tool_name, args, response, ret): pass def tool_after_callback(self, tool_name, args, response, ret): pass
def next_prompt_patcher(self, next_prompt, outcome, turn): return next_prompt def next_prompt_patcher(self, next_prompt, outcome, turn): return next_prompt
def dispatch(self, tool_name, args, response): def dispatch(self, tool_name, args, response, index=0):
method_name = f"do_{tool_name}" method_name = f"do_{tool_name}"
if hasattr(self, method_name): if hasattr(self, method_name):
args['_index'] = index
prer = yield from try_call_generator(self.tool_before_callback, tool_name, args, response) prer = yield from try_call_generator(self.tool_before_callback, tool_name, args, response)
ret = yield from try_call_generator(getattr(self, method_name), args, response) ret = yield from try_call_generator(getattr(self, method_name), args, response)
_ = yield from try_call_generator(self.tool_after_callback, tool_name, args, response, ret) _ = yield from try_call_generator(self.tool_after_callback, tool_name, args, response, ret)
@@ -61,35 +62,34 @@ def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema,
response = exhaust(response_gen) response = exhaust(response_gen)
yield response.content yield response.content
if not response.tool_calls: if not response.tool_calls: tool_calls = [{'tool_name': 'no_tool', 'args': {}}]
tool_name, args = 'no_tool', {} else: tool_calls = [{'tool_name': tc.function.name, 'args': json.loads(tc.function.arguments)}
else: for tc in response.tool_calls]
tool_call = response.tool_calls[0]
tool_name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
if tool_name == 'no_tool': pass
else:
showarg = get_pretty_json(args)
if not verbose and len(showarg) > 200: showarg = showarg[:200] + ' ...'
yield f"🛠️ **正在调用工具:** `{tool_name}` 📥**参数:**\n````text\n{showarg}\n````\n"
handler.current_turn = turn + 1
gen = handler.dispatch(tool_name, args, response)
if verbose:
yield '`````\n'
outcome = yield from gen
yield '`````\n'
else: outcome = exhaust(gen)
if outcome.next_prompt is None: return {'result': 'CURRENT_TASK_DONE', 'data': outcome.data}
if outcome.should_exit: return {'result': 'EXITED', 'data': outcome.data}
if outcome.next_prompt.startswith('未知工具'): client.last_tools = ''
next_prompt = "" next_prompt = ""
if outcome.data is not None: for ii, tc in enumerate(tool_calls):
datastr = json.dumps(outcome.data, ensure_ascii=False, default=json_default) if type(outcome.data) in [dict, list] else str(outcome.data) tool_name, args = tc['tool_name'], tc['args']
next_prompt += f"<tool_result>\n{datastr}\n</tool_result>\n\n" if tool_name == 'no_tool': pass
next_prompt += outcome.next_prompt else:
next_prompt = handler.next_prompt_patcher(next_prompt, outcome, turn+1) showarg = get_pretty_json(args)
if not verbose and len(showarg) > 200: showarg = showarg[:200] + ' ...'
yield f"🛠️ **正在调用工具:** `{tool_name}` 📥**参数:**\n````text\n{showarg}\n````\n"
handler.current_turn = turn + 1
gen = handler.dispatch(tool_name, args, response, index=ii)
if verbose:
yield '`````\n'
outcome = yield from gen
yield '`````\n'
else: outcome = exhaust(gen)
if outcome.next_prompt is None: return {'result': 'CURRENT_TASK_DONE', 'data': outcome.data}
if outcome.should_exit: return {'result': 'EXITED', 'data': outcome.data}
if outcome.next_prompt.startswith('未知工具'): client.last_tools = ''
if outcome.data is not None:
datastr = json.dumps(outcome.data, ensure_ascii=False, default=json_default) if type(outcome.data) in [dict, list] else str(outcome.data)
next_prompt += f"<tool_result>\n{datastr}\n</tool_result>\n\n"
next_prompt += outcome.next_prompt
next_prompt = handler.next_prompt_patcher(next_prompt, None, turn+1)
messages = [{"role": "user", "content": next_prompt}] messages = [{"role": "user", "content": next_prompt}]
return {'result': 'MAX_TURNS_EXCEEDED'} return {'result': 'MAX_TURNS_EXCEEDED'}

View File

@@ -5,8 +5,8 @@ if sys.stderr is None: sys.stderr = open(os.devnull, "w")
elif hasattr(sys.stderr, 'reconfigure'): sys.stderr.reconfigure(errors='replace') elif hasattr(sys.stderr, 'reconfigure'): sys.stderr.reconfigure(errors='replace')
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from llmcore import SiderLLMSession, LLMSession, ToolClient, ClaudeSession, XaiSession, build_multimodal_content from llmcore import SiderLLMSession, LLMSession, ToolClient, ClaudeSession, XaiSession, NativeToolClient, NativeClaudeSession, build_multimodal_content
from agent_loop import agent_runner_loop, StepOutcome, BaseHandler from agent_loop import agent_runner_loop
from ga import GenericAgentHandler, smart_format, get_global_memory, format_error from ga import GenericAgentHandler, smart_format, get_global_memory, format_error
script_dir = os.path.dirname(os.path.abspath(__file__)) script_dir = os.path.dirname(os.path.abspath(__file__))
@@ -44,28 +44,30 @@ class GeneraticAgent:
for k, cfg in mykeys.items(): for k, cfg in mykeys.items():
if not any(x in k for x in ['api', 'config', 'cookie']): continue if not any(x in k for x in ['api', 'config', 'cookie']): continue
try: try:
if 'claude' in k: llm_sessions += [ClaudeSession(cfg=cfg)] if 'native' in k and 'claude' in k: llm_sessions += [NativeToolClient(NativeClaudeSession(cfg=cfg))]
if 'oai' in k: llm_sessions += [LLMSession(cfg=cfg)] elif 'claude' in k: llm_sessions += [ToolClient(ClaudeSession(cfg=cfg))]
if 'xai' in k: llm_sessions += [XaiSession(cfg=cfg)] elif 'oai' in k: llm_sessions += [ToolClient(LLMSession(cfg=cfg))]
if 'sider' in k: llm_sessions += [SiderLLMSession(cfg={'apikey': cfg, 'model': x}) for x in \ elif 'xai' in k: llm_sessions += [ToolClient(XaiSession(cfg=cfg))]
elif 'sider' in k: llm_sessions += [ToolClient(SiderLLMSession(cfg={'apikey': cfg, 'model': x})) for x in \
["gemini-3.0-flash", "gpt-5.4"]] ["gemini-3.0-flash", "gpt-5.4"]]
except: pass except: pass
if len(llm_sessions) > 0: self.llmclient = ToolClient(llm_sessions, auto_save_tokens=True) self.llmclients = llm_sessions
else: self.llmclient = None
self.lock = threading.Lock() self.lock = threading.Lock()
self.history = [] self.history = []
self.task_queue = queue.Queue() self.task_queue = queue.Queue()
self.is_running, self.stop_sig = False, False self.is_running, self.stop_sig = False, False
self.llm_no = 0; self.inc_out = False self.llm_no = 0; self.inc_out = False
self.handler = None; self.verbose = True self.handler = None; self.verbose = True
self.llmclient = self.llmclients[self.llm_no]
def next_llm(self, n=-1): def next_llm(self, n=-1):
self.llm_no = ((self.llm_no + 1) if n < 0 else n) % len(self.llmclient.backends) self.llm_no = ((self.llm_no + 1) if n < 0 else n) % len(self.llmclients)
self.llmclient = self.llmclients[self.llm_no]
self.llmclient.last_tools = '' self.llmclient.last_tools = ''
def list_llms(self): return [(i, f"{type(b).__name__}/{b.default_model}", i == self.llm_no) for i, b in enumerate(self.llmclient.backends)] def list_llms(self): return [(i, f"{type(b.backend).__name__}/{b.backend.default_model}", i == self.llm_no) for i, b in enumerate(self.llmclients)]
def get_llm_name(self): def get_llm_name(self):
b = self.llmclient.backends[self.llm_no] b = self.llmclient
return f"{type(b).__name__}/{b.default_model}" return f"{type(b.backend).__name__}/{b.backend.default_model}"
def abort(self): def abort(self):
print('Abort current task...') print('Abort current task...')
@@ -95,7 +97,6 @@ class GeneraticAgent:
handler.working['passed_sessions'] = ps = self.handler.working.get('passed_sessions', 0) + 1 handler.working['passed_sessions'] = ps = self.handler.working.get('passed_sessions', 0) + 1
if ps > 0: handler.working['key_info'] += f'\n[SYSTEM] 此为 {ps} 个对话前设置的key_info若已在新任务先更新或清除工作记忆。\n' if ps > 0: handler.working['key_info'] += f'\n[SYSTEM] 此为 {ps} 个对话前设置的key_info若已在新任务先更新或清除工作记忆。\n'
self.handler = handler self.handler = handler
self.llmclient.backend = self.llmclient.backends[self.llm_no]
user_input = raw_query user_input = raw_query
if source == 'feishu' and len(self.history) > 1: # 如果有历史记录且来自飞书,注入到首轮 user_input 中(支持/restore恢复上下文 if source == 'feishu' and len(self.history) > 1: # 如果有历史记录且来自飞书,注入到首轮 user_input 中(支持/restore恢复上下文
user_input = handler._get_anchor_prompt() + f"\n\n### 用户当前消息\n{raw_query}" user_input = handler._get_anchor_prompt() + f"\n\n### 用户当前消息\n{raw_query}"

View File

@@ -1,7 +1,7 @@
[ [
{"type": "function", "function": { {"type": "function", "function": {
"name": "code_run", "name": "code_run",
"description": "代码执行器。优先使用python仅在必要系统操作时使用 powershell。注意执行的代码必须放在在回复正文中,以 ```python 或 ```powershell 代码块的形式。严禁在代码中硬编码大量数据,如有需要应通过文件读取。", "description": "代码执行器。优先使用python仅在必要系统操作时使用 powershell。注意不能同时调用多个,执行的代码在回复正文中,以 ```python 或 ```powershell 代码块的形式。严禁在代码中硬编码大量数据,如有需要应通过文件读取。",
"parameters": {"type": "object", "properties": { "parameters": {"type": "object", "properties": {
"type": {"type": "string", "enum": ["python", "powershell"], "description": "执行环境类型,默认为 python。", "default": "python"}, "type": {"type": "string", "enum": ["python", "powershell"], "description": "执行环境类型,默认为 python。", "default": "python"},
"timeout": {"type": "integer", "description": "执行超时时间(秒),默认 60。", "default": 60}, "timeout": {"type": "integer", "description": "执行超时时间(秒),默认 60。", "default": 60},

4
ga.py
View File

@@ -256,6 +256,7 @@ class GenericAgentHandler(BaseHandler):
return os.path.abspath(os.path.join(self.cwd, path)) return os.path.abspath(os.path.join(self.cwd, path))
def tool_after_callback(self, tool_name, args, response, ret): def tool_after_callback(self, tool_name, args, response, ret):
if args.get('_index', 0) > 0: return
rsumm = re.search(r"<summary>(.*?)</summary>", response.content, re.DOTALL) rsumm = re.search(r"<summary>(.*?)</summary>", response.content, re.DOTALL)
if rsumm: summary = rsumm.group(1).strip()[:200] if rsumm: summary = rsumm.group(1).strip()[:200]
else: else:
@@ -268,6 +269,7 @@ class GenericAgentHandler(BaseHandler):
def do_code_run(self, args, response): def do_code_run(self, args, response):
'''执行代码片段,有长度限制,不允许代码中放大量数据,如有需要应当通过文件读取进行。 '''执行代码片段,有长度限制,不允许代码中放大量数据,如有需要应当通过文件读取进行。
''' '''
if args.get('_index', 0) > 0: return StepOutcome("[BLANK]", next_prompt="no multi code_run in one round!")
code_type = args.get("type", "python") code_type = args.get("type", "python")
# 从 response.content 中提取代码块, 匹配 ```python ... ``` 或 ```powershell ... ``` # 从 response.content 中提取代码块, 匹配 ```python ... ``` 或 ```powershell ... ```
pattern = rf"```{code_type}\n(.*?)\n```" pattern = rf"```{code_type}\n(.*?)\n```"
@@ -502,4 +504,4 @@ def get_global_memory():
prompt += structure + '\n../memory/global_mem_insight.txt:\n' prompt += structure + '\n../memory/global_mem_insight.txt:\n'
prompt += insight + "\n" prompt += insight + "\n"
except FileNotFoundError: pass except FileNotFoundError: pass
return prompt return prompt

View File

@@ -397,25 +397,127 @@ class XaiSession:
yield f"[XaiError] {e}" yield f"[XaiError] {e}"
def reset(self): self._last_response_id = None def reset(self): self._last_response_id = None
class NativeClaudeSession:
def __init__(self, cfg):
self.api_key = cfg['apikey']; self.api_base = cfg['apibase'].rstrip('/')
self.default_model = cfg.get('model', 'claude-opus')
self.context_win = cfg.get('context_win', 24000)
self.history = []
self.system = None
self.lock = threading.Lock()
def set_system(self, system_text): self.system = system_text
def raw_ask(self, messages, tools=None, system=None, model=None, temperature=0.5, max_tokens=6144):
"""底层API调用。yields text chunksgenerator return = list[content_block]"""
model = model or self.default_model
headers = {"x-api-key": self.api_key, "Content-Type": "application/json", "anthropic-version": "2023-06-01"}
payload = {"model": model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, "stream": True}
if tools: payload["tools"] = tools
if system: payload["system"] = system
try:
resp = requests.post(auto_make_url(self.api_base, "messages"), headers=headers, json=payload, stream=True, timeout=120)
if resp.status_code != 200:
error_msg = f"Error: HTTP {resp.status_code} {resp.text[:500]}"
yield error_msg
return [{"type": "text", "text": error_msg}]
except Exception as e:
error_msg = f"Error: {e}"
yield error_msg
return [{"type": "text", "text": error_msg}]
content_blocks = []; current_block = None; tool_json_buf = ""
for line in resp.iter_lines():
if not line: continue
line = line.decode('utf-8') if isinstance(line, bytes) else line
data_str = line[6:]
if data_str.strip() == "[DONE]": break
try: evt = json.loads(data_str)
except: continue
evt_type = evt.get("type", "")
if evt_type == "content_block_start":
block = evt.get("content_block", {})
if block.get("type") == "text": current_block = {"type": "text", "text": ""}
elif block.get("type") == "tool_use":
current_block = {"type": "tool_use", "id": block.get("id", ""), "name": block.get("name", ""), "input": {}}
tool_json_buf = ""
elif evt_type == "content_block_delta":
delta = evt.get("delta", {})
if delta.get("type") == "text_delta":
text = delta.get("text", "")
if current_block: current_block["text"] += text
yield text
elif delta.get("type") == "input_json_delta": tool_json_buf += delta.get("partial_json", "")
elif evt_type == "content_block_stop":
if current_block:
if current_block["type"] == "tool_use":
try: current_block["input"] = json.loads(tool_json_buf) if tool_json_buf else {}
except: current_block["input"] = {"_raw": tool_json_buf}
content_blocks.append(current_block)
current_block = None
return content_blocks
def ask(self, msg, tools=None, model=None):
"""增量ask。msg: str|list[content_block]|dict。yields text chunks, return MockResponse"""
if isinstance(msg, str): msg = {"role": "user", "content": msg}
elif isinstance(msg, list): msg = {"role": "user", "content": msg}
with self.lock:
self.history.append(msg)
while len(self.history) > 2:
cost = sum(len(json.dumps(m, ensure_ascii=False)) for m in self.history) + len(self.system or '')
if cost <= self.context_win * 4: break
self.history.pop(0); self.history.pop(0) # 砍一对
messages = list(self.history)
content_blocks = None
gen = self.raw_ask(messages, tools, self.system, model)
try:
while True: yield next(gen)
except StopIteration as e: content_blocks = e.value or []
if content_blocks and not (len(content_blocks) == 1 and content_blocks[0].get("text", "").startswith("Error:")):
self.history.append({"role": "assistant", "content": content_blocks})
thinking = ''
text_parts = [b["text"] for b in content_blocks if b.get("type") == "text"]
content = "\n".join(text_parts).strip()
tool_calls = []
for b in content_blocks:
if b.get("type") == "tool_use":
tool_calls.append(MockToolCall(b["name"], b.get("input", {}), id=b.get("id", "")))
return MockResponse(thinking, content, tool_calls, str(content_blocks))
def openai_tools_to_claude(tools):
"""[{type:'function', function:{name,description,parameters}}] → [{name,description,input_schema}]. 幂等"""
result = []
for t in tools:
if 'input_schema' in t: result.append(t); continue # 已是claude格式
fn = t.get('function', t)
result.append({
'name': fn['name'], 'description': fn.get('description', ''),
'input_schema': fn.get('parameters', {'type': 'object', 'properties': {}})
})
return result
class MockFunction: class MockFunction:
def __init__(self, name, arguments): self.name, self.arguments = name, arguments def __init__(self, name, arguments): self.name, self.arguments = name, arguments
class MockToolCall: class MockToolCall:
def __init__(self, name, args): def __init__(self, name, args, id=''):
arg_str = json.dumps(args, ensure_ascii=False) if isinstance(args, dict) else args arg_str = json.dumps(args, ensure_ascii=False) if isinstance(args, dict) else args
self.function = MockFunction(name, arg_str) self.function = MockFunction(name, arg_str); self.id = id
class MockResponse: class MockResponse:
def __init__(self, thinking, content, tool_calls, raw): def __init__(self, thinking, content, tool_calls, raw, stop_reason='end_turn'):
self.thinking = thinking # 存放 <thinking> 内部的思维过程 self.thinking = thinking; self.content = content
self.content = content # 存放去除标签后的纯文本回复 self.tool_calls = tool_calls; self.raw = raw
self.tool_calls = tool_calls # 存放 MockToolCall 列表 或 None self.stop_reason = 'tool_use' if tool_calls else stop_reason
self.raw = raw
def __repr__(self): def __repr__(self):
return f"<MockResponse thinking={bool(self.thinking)}, content='{self.content}', tools={bool(self.tool_calls)}>" return f"<MockResponse thinking={bool(self.thinking)}, content='{self.content}', tools={bool(self.tool_calls)}>"
class ToolClient: class ToolClient:
def __init__(self, backends, auto_save_tokens=False): def __init__(self, backends, auto_save_tokens=True):
if isinstance(backends, list): self.backends = backends if isinstance(backends, list): self.backends = backends
else: self.backends = [backends] else: self.backends = [backends]
self.backend = self.backends[0] self.backend = self.backends[0]
@@ -424,8 +526,6 @@ class ToolClient:
self.total_cd_tokens = 0 self.total_cd_tokens = 0
def chat(self, messages, tools=None): def chat(self, messages, tools=None):
script_dir = os.path.dirname(os.path.abspath(__file__))
log_path = os.path.join(script_dir, f'./temp/model_responses_{os.getpid()}.txt')
if self._should_use_structured_messages(messages): if self._should_use_structured_messages(messages):
backend_messages = self._build_backend_messages(messages, tools) backend_messages = self._build_backend_messages(messages, tools)
print("Structured prompt length:", sum(self._estimate_content_len(m.get("content")) for m in backend_messages), 'chars') print("Structured prompt length:", sum(self._estimate_content_len(m.get("content")) for m in backend_messages), 'chars')
@@ -436,8 +536,7 @@ class ToolClient:
print("Full prompt length:", len(full_prompt), 'chars') print("Full prompt length:", len(full_prompt), 'chars')
prompt_log = full_prompt prompt_log = full_prompt
gen = self.backend.ask(full_prompt, stream=True) gen = self.backend.ask(full_prompt, stream=True)
with open(log_path, 'a', encoding='utf-8', errors="replace") as f: _write_llm_log('Prompt', prompt_log)
f.write(f"=== Prompt === {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n{prompt_log}\n")
raw_text = ''; summarytag = '[NextWillSummary]' raw_text = ''; summarytag = '[NextWillSummary]'
for chunk in gen: for chunk in gen:
raw_text += chunk raw_text += chunk
@@ -445,8 +544,7 @@ class ToolClient:
print('Complete response received.') print('Complete response received.')
if raw_text.endswith(summarytag): if raw_text.endswith(summarytag):
self.last_tools = ''; raw_text = raw_text[:-len(summarytag)] self.last_tools = ''; raw_text = raw_text[:-len(summarytag)]
with open(log_path, 'a', encoding='utf-8', errors="replace") as f: _write_llm_log('Response', raw_text)
f.write(f"=== Response === {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n{raw_text}\n\n")
return self._parse_mixed_response(raw_text) return self._parse_mixed_response(raw_text)
def _should_use_structured_messages(self, messages): def _should_use_structured_messages(self, messages):
@@ -474,7 +572,7 @@ class ToolClient:
请按照以下步骤思考并行动,标签之间需要回车换行: 请按照以下步骤思考并行动,标签之间需要回车换行:
1. **思考**: 在 `<thinking>` 标签中先进行思考,分析现状和策略。 1. **思考**: 在 `<thinking>` 标签中先进行思考,分析现状和策略。
2. **总结**: 在 `<summary>` 中输出*极为简短*的高度概括的单行(<30字物理快照包括上次工具调用结果产生的新信息+本次工具调用意图。此内容将进入长期工作记忆,记录关键信息,严禁输出无实际信息增量的描述。 2. **总结**: 在 `<summary>` 中输出*极为简短*的高度概括的单行(<30字物理快照包括上次工具调用结果产生的新信息+本次工具调用意图。此内容将进入长期工作记忆,记录关键信息,严禁输出无实际信息增量的描述。
3. **行动**: 如需调用工具,请在回复正文之后输出一个 **<tool_use>块**,然后结束,我会稍后给你返回<tool_result>块。 3. **行动**: 如需调用工具,请在回复正文之后输出一个(或多个)**<tool_use>块**,然后结束,我会稍后给你返回<tool_result>块。
格式: ```<tool_use>\n{{"name": "工具名", "arguments": {{参数}}}}\n</tool_use>\n``` 格式: ```<tool_use>\n{{"name": "工具名", "arguments": {{参数}}}}\n</tool_use>\n```
### 可用工具库(已挂载,持续有效) ### 可用工具库(已挂载,持续有效)
@@ -589,7 +687,13 @@ class ToolClient:
print(e['err']) print(e['err'])
if 'bad_json' in e: tool_calls.append(MockToolCall('bad_json', {'msg': e['bad_json']})) if 'bad_json' in e: tool_calls.append(MockToolCall('bad_json', {'msg': e['bad_json']}))
content = remaining_text.strip() content = remaining_text.strip()
return MockResponse(thinking, content, tool_calls[-1:], text) return MockResponse(thinking, content, tool_calls, text)
def _write_llm_log(label, content):
log_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), f'temp/model_responses_{os.getpid()}.txt')
ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
with open(log_path, 'a', encoding='utf-8', errors='replace') as f:
f.write(f"=== {label} === {ts}\n{content}\n\n")
def tryparse(json_str): def tryparse(json_str):
try: return json.loads(json_str) try: return json.loads(json_str)
@@ -602,30 +706,43 @@ def tryparse(json_str):
if '}' in json_str: json_str = json_str[:json_str.rfind('}') + 1] if '}' in json_str: json_str = json_str[:json_str.rfind('}') + 1]
return json.loads(json_str) return json.loads(json_str)
if __name__ == "__main__":
sider_cookie = mykeys.get("sider_cookie")
oai_configs = {
k: v for k, v in mykeys.items() if k.startswith("oai_config") and v
}
google_api_key = mykeys.get("google_api_key")
cfg = oai_configs.get("oai_config")
llmclient = ToolClient(LLMSession(cfg))
def get_final(gen):
try:
while True: print('mid:', next(gen))
except StopIteration as e:
return e.value
response = get_final(llmclient.chat(
messages=[{"role": "user", "content": "我的IP是多少"}],
tools=[{"name": "get_ip", "parameters": {}}]
))
print(f"思考: {response.thinking}")
if response.tool_calls:
cmd = response.tool_calls[0]
print(f"调用: {cmd.function.name} 参数: {cmd.function.arguments}")
response = get_final(llmclient.chat( class NativeToolClient:
messages=[{"role": "user", "content": "<tool_result>10.176.45.12</tool_result>"}] THINKING_PROMPT = """
)) ### 行动规范(持续有效)
print(response.content) 每次回复请遵循:
1. 在 <thinking> 标签中先分析现状和策略
2. 在 <summary> 中输出极简单行(<30字物理快照上次结果新信息+本次意图。此内容进入长期工作记忆。
3. 如需调用工具,直接使用工具调用能力,然后结束回复。
""".strip()
def __init__(self, backend):
self.backend = backend
self.backend.system = self.THINKING_PROMPT
self.tools = {}
def set_system(self, extra_system):
combined = f"{extra_system}\n\n{self.THINKING_PROMPT}" if extra_system else self.THINKING_PROMPT
self.backend.system = combined
def chat(self, messages, tools=None):
if tools: self.tools = openai_tools_to_claude(tools) if isinstance(self.backend, NativeClaudeSession) else tools
combined_content = []; resp = None
for msg in messages:
c = msg.get('content', '')
if isinstance(c, str): combined_content.append({"type": "text", "text": c})
elif isinstance(c, list) or isinstance(c, dict): combined_content.extend(c)
merged = {"role": "user", "content": combined_content}
_write_llm_log('Prompt', json.dumps(merged, ensure_ascii=False, indent=2))
gen = self.backend.ask(merged, self.tools);
try:
while True:
chunk = next(gen); yield chunk
except StopIteration as e: resp = e.value
print('Complete response received.')
if resp:
_write_llm_log('Response', resp.raw)
text = resp.content
think_match = re.search(r'<thinking>(.*?)</thinking>', text, re.DOTALL)
if think_match:
resp.thinking = think_match.group(1).strip()
text = re.sub(r'<thinking>.*?</thinking>', '', text, flags=re.DOTALL)
resp.content = text.strip()
return resp