From 6f6d9f65701fc066001c2cd4afff6f6be4eb1b03 Mon Sep 17 00:00:00 2001 From: Liang Jiaqing Date: Sun, 22 Mar 2026 22:19:46 +0800 Subject: [PATCH] feat: add NativeOAISession + minor fixes (code_run script fallback, thinking prompt tags) --- agentmain.py | 3 +- ga.py | 2 +- llmcore.py | 73 +++++++++++++++++++++++++++++++++++++++++++++-- mykey_template.py | 13 ++++++++- 4 files changed, 86 insertions(+), 5 deletions(-) diff --git a/agentmain.py b/agentmain.py index d990bd7..42d7a57 100644 --- a/agentmain.py +++ b/agentmain.py @@ -5,7 +5,7 @@ if sys.stderr is None: sys.stderr = open(os.devnull, "w") elif hasattr(sys.stderr, 'reconfigure'): sys.stderr.reconfigure(errors='replace') sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from llmcore import SiderLLMSession, LLMSession, ToolClient, ClaudeSession, XaiSession, NativeToolClient, NativeClaudeSession, build_multimodal_content +from llmcore import SiderLLMSession, LLMSession, ToolClient, ClaudeSession, XaiSession, NativeToolClient, NativeClaudeSession, build_multimodal_content, NativeOAISession from agent_loop import agent_runner_loop from ga import GenericAgentHandler, smart_format, get_global_memory, format_error @@ -45,6 +45,7 @@ class GeneraticAgent: if not any(x in k for x in ['api', 'config', 'cookie']): continue try: if 'native' in k and 'claude' in k: llm_sessions += [NativeToolClient(NativeClaudeSession(cfg=cfg))] + elif 'native' in k and 'oai' in k: llm_sessions += [NativeToolClient(NativeOAISession(cfg=cfg))] elif 'claude' in k: llm_sessions += [ToolClient(ClaudeSession(cfg=cfg))] elif 'oai' in k: llm_sessions += [ToolClient(LLMSession(cfg=cfg))] elif 'xai' in k: llm_sessions += [ToolClient(XaiSession(cfg=cfg))] diff --git a/ga.py b/ga.py index 56b4e32..c023666 100644 --- a/ga.py +++ b/ga.py @@ -276,7 +276,7 @@ class GenericAgentHandler(BaseHandler): matches = re.findall(pattern, response.content, re.DOTALL) warning = "" if not matches: - code = args.get("code") + code = args.get("code") or args.get("script") if not code: return StepOutcome(None, next_prompt=f"【系统错误】:你调用了 code_run,但未在先在回复正文中提供 ```{code_type} 代码块。请重新输出代码并附带工具调用。") warning = "\n下次要记得先在回复正文中提供代码块,而不是放在参数中" else: code = matches[-1].strip() # 提取最后一个代码块(通常是模型修正后的最终逻辑) diff --git a/llmcore.py b/llmcore.py index f46f8d6..03ee440 100644 --- a/llmcore.py +++ b/llmcore.py @@ -398,6 +398,75 @@ class XaiSession: def reset(self): self._last_response_id = None +class NativeOAISession: + def __init__(self, cfg): + self.api_key = cfg['apikey']; self.api_base = cfg['apibase'].rstrip('/') + self.default_model = cfg.get('model', 'gpt-4o') + 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, **kw): + """OpenAI streaming. yields text chunks, generator return = list[content_block]""" + model = model or self.default_model + headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"} + msgs = ([{"role": "system", "content": system}] if system else []) + messages + payload = {"model": model, "messages": msgs, "temperature": temperature, "max_tokens": max_tokens, "stream": True} + if tools: payload["tools"] = tools + try: + resp = requests.post(auto_make_url(self.api_base, "chat/completions"), headers=headers, json=payload, stream=True, timeout=120) + if resp.status_code != 200: + err = f"Error: HTTP {resp.status_code} {resp.text[:500]}"; yield err; return [{"type": "text", "text": err}] + except Exception as e: + err = f"Error: {e}"; yield err; return [{"type": "text", "text": err}] + content_text = ""; tc_buf = {} # index -> {id, name, args_str} + for line in resp.iter_lines(): + if not line: continue + line = line.decode('utf-8', errors='replace') if isinstance(line, bytes) else line + if not line.startswith("data: "): continue + data_str = line[6:] + if data_str.strip() == "[DONE]": break + try: evt = json.loads(data_str) + except: continue + delta = evt.get("choices", [{}])[0].get("delta", {}) + if delta.get("content"): + text = delta["content"]; content_text += text; yield text + for tc in delta.get("tool_calls", []): + idx = tc.get("index", 0) + if idx not in tc_buf: tc_buf[idx] = {"id": tc.get("id", ""), "name": "", "args": ""} + if tc.get("function", {}).get("name"): tc_buf[idx]["name"] = tc["function"]["name"] + if tc.get("function", {}).get("arguments"): tc_buf[idx]["args"] += tc["function"]["arguments"] + blocks = [] + if content_text: blocks.append({"type": "text", "text": content_text}) + for idx in sorted(tc_buf): + tc = tc_buf[idx] + try: inp = json.loads(tc["args"]) if tc["args"] else {} + except: inp = {"_raw": tc["args"]} + blocks.append({"type": "tool_use", "id": tc["id"], "name": tc["name"], "input": inp}) + return blocks + + def ask(self, msg, tools=None, model=None, **kw): + """Managed ask with history. 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}) + text_parts = [b["text"] for b in content_blocks if b.get("type") == "text"] + content = "\n".join(text_parts).strip() + tool_calls = [MockToolCall(b["name"], b.get("input", {}), id=b.get("id", "")) for b in content_blocks if b.get("type") == "tool_use"] + return MockResponse("", content, tool_calls, str(content_blocks)) class NativeClaudeSession: @@ -711,8 +780,8 @@ class NativeToolClient: THINKING_PROMPT = """ ### 行动规范(持续有效) 每次回复请遵循: -1. 在 标签中先分析现状和策略 -2. 在 中输出极简单行(<30字)物理快照:上次结果新信息+本次意图。此内容进入长期工作记忆。 +1. 在 标签中先分析现状和策略 +2. 在 中输出极简单行(<30字)物理快照:上次结果新信息+本次意图。此内容进入长期工作记忆。 3. 如需调用工具,直接使用工具调用能力,然后结束回复。 """.strip() def __init__(self, backend): diff --git a/mykey_template.py b/mykey_template.py index 39ff877..1c12218 100644 --- a/mykey_template.py +++ b/mykey_template.py @@ -40,7 +40,8 @@ claude_config = { } # ── Claude Native API ─────────────────────────────────────────────────────────── -# key命名同时含 'native' 和 'claude' 触发 NativeClaudeSession(原生Anthropic协议) +# key命名同时含 'native' 和 'claude' 触发 NativeClaudeSession +# 原生工具调用格式,缓解弱模型指令遵循问题,但更耗token native_claude_config = { 'apikey': 'sk-ant-...', # Anthropic原生apikey 'apibase': 'https://api.anthropic.com', @@ -48,6 +49,16 @@ native_claude_config = { # 'context_win': 24000, } +# ── OpenAI-compatible Native API ───────────────────────────────────────────── +# key命名同时含 'native' 和 'oai' 触发 NativeOAISession +# 原生工具调用格式,缓解弱模型指令遵循问题,但更耗token +native_oai_config = { + 'apikey': 'sk-...', + 'apibase': 'http://your-proxy:2001', + 'model': 'gpt-4o', + # 'context_win': 24000, +} + # ── Sider ─────────────────────────────────────────────────────────────────────── # key命名含 'sider' 触发 SiderLLMSession(需安装 sider_ai_api 包) #sider_cookie = 'token=Bearer%20eyJhbGciOiJIUz...'