feat: add NativeOAISession + minor fixes (code_run script fallback, thinking prompt tags)

This commit is contained in:
Liang Jiaqing
2026-03-22 22:19:46 +08:00
parent fd5150ffad
commit 6f6d9f6570
4 changed files with 86 additions and 5 deletions

View File

@@ -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') 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, 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 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
@@ -45,6 +45,7 @@ class GeneraticAgent:
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 'native' in k and 'claude' in k: llm_sessions += [NativeToolClient(NativeClaudeSession(cfg=cfg))] 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 'claude' in k: llm_sessions += [ToolClient(ClaudeSession(cfg=cfg))]
elif 'oai' in k: llm_sessions += [ToolClient(LLMSession(cfg=cfg))] elif 'oai' in k: llm_sessions += [ToolClient(LLMSession(cfg=cfg))]
elif 'xai' in k: llm_sessions += [ToolClient(XaiSession(cfg=cfg))] elif 'xai' in k: llm_sessions += [ToolClient(XaiSession(cfg=cfg))]

2
ga.py
View File

@@ -276,7 +276,7 @@ class GenericAgentHandler(BaseHandler):
matches = re.findall(pattern, response.content, re.DOTALL) matches = re.findall(pattern, response.content, re.DOTALL)
warning = "" warning = ""
if not matches: 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} 代码块。请重新输出代码并附带工具调用。") if not code: return StepOutcome(None, next_prompt=f"【系统错误】:你调用了 code_run但未在先在回复正文中提供 ```{code_type} 代码块。请重新输出代码并附带工具调用。")
warning = "\n下次要记得先在回复正文中提供代码块,而不是放在参数中" warning = "\n下次要记得先在回复正文中提供代码块,而不是放在参数中"
else: code = matches[-1].strip() # 提取最后一个代码块(通常是模型修正后的最终逻辑) else: code = matches[-1].strip() # 提取最后一个代码块(通常是模型修正后的最终逻辑)

View File

@@ -398,6 +398,75 @@ class XaiSession:
def reset(self): self._last_response_id = None 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: class NativeClaudeSession:
@@ -711,8 +780,8 @@ class NativeToolClient:
THINKING_PROMPT = """ THINKING_PROMPT = """
### 行动规范(持续有效) ### 行动规范(持续有效)
每次回复请遵循: 每次回复请遵循:
1. 在 <thinking> 标签中先分析现状和策略 1. 在 <thinking></thinking> 标签中先分析现状和策略
2. 在 <summary> 中输出极简单行(<30字物理快照上次结果新信息+本次意图。此内容进入长期工作记忆。 2. 在 <summary></summary> 中输出极简单行(<30字物理快照上次结果新信息+本次意图。此内容进入长期工作记忆。
3. 如需调用工具,直接使用工具调用能力,然后结束回复。 3. 如需调用工具,直接使用工具调用能力,然后结束回复。
""".strip() """.strip()
def __init__(self, backend): def __init__(self, backend):

View File

@@ -40,7 +40,8 @@ claude_config = {
} }
# ── Claude Native API ─────────────────────────────────────────────────────────── # ── Claude Native API ───────────────────────────────────────────────────────────
# key命名同时含 'native' 和 'claude' 触发 NativeClaudeSession原生Anthropic协议 # key命名同时含 'native' 和 'claude' 触发 NativeClaudeSession
# 原生工具调用格式缓解弱模型指令遵循问题但更耗token
native_claude_config = { native_claude_config = {
'apikey': 'sk-ant-...', # Anthropic原生apikey 'apikey': 'sk-ant-...', # Anthropic原生apikey
'apibase': 'https://api.anthropic.com', 'apibase': 'https://api.anthropic.com',
@@ -48,6 +49,16 @@ native_claude_config = {
# 'context_win': 24000, # '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 ─────────────────────────────────────────────────────────────────────── # ── Sider ───────────────────────────────────────────────────────────────────────
# key命名含 'sider' 触发 SiderLLMSession需安装 sider_ai_api 包) # key命名含 'sider' 触发 SiderLLMSession需安装 sider_ai_api 包)
#sider_cookie = 'token=Bearer%20eyJhbGciOiJIUz...' #sider_cookie = 'token=Bearer%20eyJhbGciOiJIUz...'