add NativeClaudeSession+NativeToolClient, support parallel tool calls, fix tool_calls[-1:], add llm log
This commit is contained in:
@@ -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,20 +62,20 @@ 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)
|
|
||||||
|
|
||||||
|
next_prompt = ""
|
||||||
|
for ii, tc in enumerate(tool_calls):
|
||||||
|
tool_name, args = tc['tool_name'], tc['args']
|
||||||
if tool_name == 'no_tool': pass
|
if tool_name == 'no_tool': pass
|
||||||
else:
|
else:
|
||||||
showarg = get_pretty_json(args)
|
showarg = get_pretty_json(args)
|
||||||
if not verbose and len(showarg) > 200: showarg = showarg[:200] + ' ...'
|
if not verbose and len(showarg) > 200: showarg = showarg[:200] + ' ...'
|
||||||
yield f"🛠️ **正在调用工具:** `{tool_name}` 📥**参数:**\n````text\n{showarg}\n````\n"
|
yield f"🛠️ **正在调用工具:** `{tool_name}` 📥**参数:**\n````text\n{showarg}\n````\n"
|
||||||
handler.current_turn = turn + 1
|
handler.current_turn = turn + 1
|
||||||
gen = handler.dispatch(tool_name, args, response)
|
gen = handler.dispatch(tool_name, args, response, index=ii)
|
||||||
if verbose:
|
if verbose:
|
||||||
yield '`````\n'
|
yield '`````\n'
|
||||||
outcome = yield from gen
|
outcome = yield from gen
|
||||||
@@ -85,11 +86,10 @@ def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema,
|
|||||||
if outcome.should_exit: return {'result': 'EXITED', 'data': outcome.data}
|
if outcome.should_exit: return {'result': 'EXITED', 'data': outcome.data}
|
||||||
if outcome.next_prompt.startswith('未知工具'): client.last_tools = ''
|
if outcome.next_prompt.startswith('未知工具'): client.last_tools = ''
|
||||||
|
|
||||||
next_prompt = ""
|
|
||||||
if outcome.data is not None:
|
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)
|
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 += f"<tool_result>\n{datastr}\n</tool_result>\n\n"
|
||||||
next_prompt += outcome.next_prompt
|
next_prompt += outcome.next_prompt
|
||||||
next_prompt = handler.next_prompt_patcher(next_prompt, outcome, turn+1)
|
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'}
|
||||||
|
|||||||
27
agentmain.py
27
agentmain.py
@@ -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}"
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
2
ga.py
2
ga.py
@@ -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```"
|
||||||
|
|||||||
201
llmcore.py
201
llmcore.py
@@ -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 chunks,generator 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")
|
class NativeToolClient:
|
||||||
oai_configs = {
|
THINKING_PROMPT = """
|
||||||
k: v for k, v in mykeys.items() if k.startswith("oai_config") and v
|
### 行动规范(持续有效)
|
||||||
}
|
每次回复请遵循:
|
||||||
google_api_key = mykeys.get("google_api_key")
|
1. 在 <thinking> 标签中先分析现状和策略
|
||||||
cfg = oai_configs.get("oai_config")
|
2. 在 <summary> 中输出极简单行(<30字)物理快照:上次结果新信息+本次意图。此内容进入长期工作记忆。
|
||||||
llmclient = ToolClient(LLMSession(cfg))
|
3. 如需调用工具,直接使用工具调用能力,然后结束回复。
|
||||||
def get_final(gen):
|
""".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:
|
try:
|
||||||
while True: print('mid:', next(gen))
|
while True:
|
||||||
except StopIteration as e:
|
chunk = next(gen); yield chunk
|
||||||
return e.value
|
except StopIteration as e: resp = e.value
|
||||||
|
print('Complete response received.')
|
||||||
response = get_final(llmclient.chat(
|
if resp:
|
||||||
messages=[{"role": "user", "content": "我的IP是多少"}],
|
_write_llm_log('Response', resp.raw)
|
||||||
tools=[{"name": "get_ip", "parameters": {}}]
|
text = resp.content
|
||||||
))
|
think_match = re.search(r'<thinking>(.*?)</thinking>', text, re.DOTALL)
|
||||||
print(f"思考: {response.thinking}")
|
if think_match:
|
||||||
if response.tool_calls:
|
resp.thinking = think_match.group(1).strip()
|
||||||
cmd = response.tool_calls[0]
|
text = re.sub(r'<thinking>.*?</thinking>', '', text, flags=re.DOTALL)
|
||||||
print(f"调用: {cmd.function.name} 参数: {cmd.function.arguments}")
|
resp.content = text.strip()
|
||||||
|
return resp
|
||||||
response = get_final(llmclient.chat(
|
|
||||||
messages=[{"role": "user", "content": "<tool_result>10.176.45.12</tool_result>"}]
|
|
||||||
))
|
|
||||||
print(response.content)
|
|
||||||
Reference in New Issue
Block a user