feat: add NativeOAISession + minor fixes (code_run script fallback, thinking prompt tags)
This commit is contained in:
@@ -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))]
|
||||
|
||||
2
ga.py
2
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() # 提取最后一个代码块(通常是模型修正后的最终逻辑)
|
||||
|
||||
73
llmcore.py
73
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. 在 <thinking> 标签中先分析现状和策略
|
||||
2. 在 <summary> 中输出极简单行(<30字)物理快照:上次结果新信息+本次意图。此内容进入长期工作记忆。
|
||||
1. 在 <thinking></thinking> 标签中先分析现状和策略
|
||||
2. 在 <summary></summary> 中输出极简单行(<30字)物理快照:上次结果新信息+本次意图。此内容进入长期工作记忆。
|
||||
3. 如需调用工具,直接使用工具调用能力,然后结束回复。
|
||||
""".strip()
|
||||
def __init__(self, backend):
|
||||
|
||||
@@ -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...'
|
||||
|
||||
Reference in New Issue
Block a user