refactor: remove XAI/Gemini sessions, add MixinSession fallback, improve error handling
- Remove GeminiSession and XaiSession from llmcore.py - Add MixinSession: multi-endpoint fallback with exponential backoff - ga.py: handle empty/incomplete/max_tokens responses - Update mykey_template.py and GETTING_STARTED.md accordingly
This commit is contained in:
@@ -91,7 +91,7 @@ native_claude_config = {
|
||||
}
|
||||
```
|
||||
|
||||
> 💡 还支持 `native_oai_config`(OpenAI 标准工具调用)、`xai_config`(Grok)、`sider_cookie`(Sider)等,详见 `mykey_template.py` 中的注释。
|
||||
> 💡 还支持 `native_oai_config`(OpenAI 标准工具调用)、`sider_cookie`(Sider)等,详见 `mykey_template.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, NativeOAISession
|
||||
from llmcore import SiderLLMSession, LLMSession, ToolClient, ClaudeSession, MixinSession, NativeToolClient, NativeClaudeSession, build_multimodal_content, NativeOAISession
|
||||
from agent_loop import agent_runner_loop
|
||||
from ga import GenericAgentHandler, smart_format, get_global_memory, format_error
|
||||
|
||||
@@ -48,10 +48,14 @@ class GeneraticAgent:
|
||||
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))]
|
||||
elif 'sider' in k: llm_sessions += [ToolClient(SiderLLMSession(cfg={'apikey': cfg, 'model': x})) for x in \
|
||||
["gemini-3.0-flash", "gpt-5.4"]]
|
||||
elif 'mixin' in k: llm_sessions += [{'mixin_cfg': cfg}]
|
||||
except: pass
|
||||
for i, s in enumerate(llm_sessions):
|
||||
if isinstance(s, dict) and 'mixin_cfg' in s:
|
||||
try: llm_sessions[i] = ToolClient(MixinSession(llm_sessions, s['mixin_cfg']))
|
||||
except Exception as e: print(f'[WARN] Failed to init MixinSession with cfg {s["mixin_cfg"]}: {e}')
|
||||
self.llmclients = llm_sessions
|
||||
self.lock = threading.Lock()
|
||||
self.history = []
|
||||
|
||||
8
ga.py
8
ga.py
@@ -441,11 +441,13 @@ class GenericAgentHandler(BaseHandler):
|
||||
二次确认仅在回复几乎只包含<thinking>/<summary>和一段大代码块时触发。
|
||||
'''
|
||||
content = getattr(response, 'content', '') or ""
|
||||
# 1. 空回复保护:要求模型重新生成内容或调用工具
|
||||
if not response or not content.strip():
|
||||
yield "[Warn] LLM returned an empty response. Retrying...\n"
|
||||
next_prompt = "[System] 回复为空,请重新生成内容或调用工具。"
|
||||
return StepOutcome({}, next_prompt=next_prompt, should_exit=False)
|
||||
return StepOutcome({}, next_prompt="[System] Blank response, regenerate and tooluse", should_exit=False)
|
||||
if '流异常中断,未收到完整响应 !!!]' in content:
|
||||
return StepOutcome({}, next_prompt="[System] Incomplete response. Regenerate and tooluse.", should_exit=False)
|
||||
if 'max_tokens !!!]' in content:
|
||||
return StepOutcome({}, next_prompt="[System] max_tokens limit reached. Use multi small steps to do it.", should_exit=False)
|
||||
# 2. 检测“包含较大代码块但未调用工具”的情况
|
||||
# 这里通过三引号代码块 + 最少字符数的方式粗略判断“大段代码”
|
||||
code_block_pattern = r"```[a-zA-Z0-9_]*\n[\s\S]{100,}?```"
|
||||
|
||||
104
llmcore.py
104
llmcore.py
@@ -457,75 +457,6 @@ class LLMSession:
|
||||
return ''.join(list(_ask_gen()))
|
||||
|
||||
|
||||
class GeminiSession:
|
||||
def __init__(self, cfg):
|
||||
self.api_key = cfg.get('apikey')
|
||||
if not self.api_key: raise ValueError("google_api_key 未配置或为空,请在 mykey.py 中设置")
|
||||
self.default_model = cfg.get('model', 'gemini-2.0-flash-001')
|
||||
p = cfg.get('proxy', proxy)
|
||||
self.proxies = {"http":p, "https":p} if p else None
|
||||
def ask(self, prompt, model=None, stream=False):
|
||||
if model is None: model = self.default_model
|
||||
url = f"https://generativelanguage.googleapis.com/v1/models/{model}:generateContent?key={self.api_key}"
|
||||
headers = {"Content-Type":"application/json"}
|
||||
data = {"contents":[{"role":"user","parts":[{"text":prompt}]}]}
|
||||
try:
|
||||
kw = {"headers":headers, "json":data, "timeout":60, 'proxies': self.proxies}
|
||||
r = requests.post(url, **kw)
|
||||
except Exception as e:
|
||||
return f"[GeminiError] request failed: {e}"
|
||||
if r.status_code != 200:
|
||||
body = r.text[:500].replace("\n"," ")
|
||||
return f"[GeminiError] HTTP {r.status_code}: {body}"
|
||||
try:
|
||||
obj = r.json(); cands = obj.get("candidates") or []
|
||||
if not cands: return "[GeminiError] empty candidates"
|
||||
parts = (cands[0].get("content") or {}).get("parts") or []
|
||||
full_text = "".join(p.get("text","") for p in parts)
|
||||
except Exception as e:
|
||||
return f"[GeminiError] invalid response format: {e}"
|
||||
return iter([full_text]) if stream else full_text
|
||||
|
||||
class XaiSession:
|
||||
def __init__(self, cfg):
|
||||
import xai_sdk
|
||||
from xai_sdk.chat import user, system
|
||||
self._user, self._system = user, system
|
||||
self.default_model = cfg.get('model', 'grok-4-1-fast-non-reasoning')
|
||||
self._last_response_id = None # 多轮对话链
|
||||
os.environ["XAI_API_KEY"] = cfg['apikey']
|
||||
proxy = cfg.get('proxy', 'http://127.0.0.1:2082')
|
||||
if not proxy.startswith("http"): proxy = f"http://{proxy}"
|
||||
os.environ.setdefault("grpc_proxy", proxy)
|
||||
self._client = xai_sdk.Client()
|
||||
def ask(self, prompt, model=None, system_prompt=None, stream=False):
|
||||
"""发送消息,自动串联多轮对话;stream=True返回生成器"""
|
||||
mdl = model or self.default_model
|
||||
try:
|
||||
kw = dict(model=mdl, store_messages=True)
|
||||
if self._last_response_id: kw["previous_response_id"] = self._last_response_id
|
||||
chat = self._client.chat.create(**kw)
|
||||
if system_prompt: chat.append(self._system(system_prompt))
|
||||
chat.append(self._user(prompt))
|
||||
if stream: return self._stream(chat)
|
||||
resp = chat.sample()
|
||||
self._last_response_id = resp.id
|
||||
return resp.content
|
||||
except Exception as e:
|
||||
err = f"[XaiError] {e}"
|
||||
return iter([err]) if stream else err
|
||||
def _stream(self, chat):
|
||||
try:
|
||||
last_resp = None
|
||||
for resp, chunk in chat.stream():
|
||||
last_resp = resp
|
||||
if chunk and chunk.content: yield chunk.content
|
||||
if last_resp and hasattr(last_resp, 'id'): self._last_response_id = last_resp.id
|
||||
except Exception as e:
|
||||
yield f"[XaiError] {e}"
|
||||
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('/')
|
||||
@@ -869,6 +800,41 @@ def tryparse(json_str):
|
||||
return json.loads(json_str)
|
||||
|
||||
|
||||
class MixinSession:
|
||||
"""Multi-session fallback with exponential backoff on Error: detection."""
|
||||
def __init__(self, all_sessions, cfg):
|
||||
self._retries, self._base_delay = cfg.get('max_retries', 3), cfg.get('base_delay', 1.5)
|
||||
self._sessions = [all_sessions[i].backend for i in cfg.get('llm_nos', [])]
|
||||
assert 'Native' not in self._sessions[0].__class__.__name__
|
||||
assert len(set(type(s) for s in self._sessions)) == 1, f'MixinSession: all sessions must be same type, got {[type(s).__name__ for s in self._sessions]}'
|
||||
self._orig_raw_asks = [s.raw_ask for s in self._sessions]
|
||||
self._sessions[0].raw_ask = self._raw_ask
|
||||
self.default_model = getattr(self._sessions[0], 'default_model', None)
|
||||
def __getattr__(self, name): return getattr(self._sessions[0], name)
|
||||
@property
|
||||
def primary(self): return self._sessions[0]
|
||||
def _raw_ask(self, *args, **kwargs):
|
||||
last_err = None
|
||||
for attempt in range(self._retries + 1):
|
||||
gen = self._orig_raw_asks[attempt % len(self._sessions)](*args, **kwargs)
|
||||
try: first = next(gen)
|
||||
except StopIteration as e: return e.value or []
|
||||
if isinstance(first, str) and first.startswith('Error:'):
|
||||
last_err = first
|
||||
for _ in gen: pass # drain
|
||||
if attempt < self._retries:
|
||||
delay = min(30, self._base_delay * (2 ** attempt))
|
||||
print(f'[MixinSession] {first[:80]}, retry {attempt+1}/{self._retries} in {delay:.1f}s')
|
||||
time.sleep(delay); continue
|
||||
else:
|
||||
yield first
|
||||
try:
|
||||
while True: yield next(gen)
|
||||
except StopIteration as e: return e.value or []
|
||||
yield last_err or 'Error: all retries exhausted'
|
||||
return [{'type': 'text', 'text': last_err}]
|
||||
|
||||
|
||||
class NativeToolClient:
|
||||
THINKING_PROMPT = """
|
||||
### 行动规范(持续有效)
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
# 填完整路径 'http://host:2001/v1/chat/completions' → 直接使用,不再拼接
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# ── Mixin (实验性) ───────────────────────────────────────────────────────────────
|
||||
# key命名含 'mixin' 触发 MixinSession:多key/endpoint自动fallback + 指数退避重试
|
||||
# 约束:引用的session须同类型,不支持Native
|
||||
# mixin_config = {'llm_nos': [1, 2], 'max_retries': 3, 'base_delay': 1.5} # 序号含自身(此处mixin=0)
|
||||
|
||||
# ── OpenAI-compatible (chat/completions or responses API) ──────────────────────
|
||||
# key命名含 'oai' 触发 LLMSession
|
||||
oai_config = {
|
||||
@@ -63,13 +68,6 @@ native_oai_config = {
|
||||
# key命名含 'sider' 触发 SiderLLMSession(需安装 sider_ai_api 包)
|
||||
#sider_cookie = 'token=Bearer%20eyJhbGciOiJIUz...'
|
||||
|
||||
# ── xAI Grok ────────────────────────────────────────────────────────────────────
|
||||
# key命名含 'xai' 触发 XaiSession(需安装 xai_sdk 包)
|
||||
# xai_config = {
|
||||
# 'apikey': 'xai-...',
|
||||
# 'model': 'grok-4-1-fast-non-reasoning',
|
||||
# 'proxy': 'http://127.0.0.1:2082',
|
||||
# }
|
||||
|
||||
# If you need them
|
||||
# tg_bot_token = '84102K2gYZ...'
|
||||
|
||||
Reference in New Issue
Block a user