diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 9902969..514c11d 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -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` 中的注释。 ### 关键规则 diff --git a/agentmain.py b/agentmain.py index 4cb7ef4..c98d15a 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, 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 = [] diff --git a/ga.py b/ga.py index 499c321..aa2be1e 100644 --- a/ga.py +++ b/ga.py @@ -441,11 +441,13 @@ class GenericAgentHandler(BaseHandler): 二次确认仅在回复几乎只包含/和一段大代码块时触发。 ''' 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,}?```" diff --git a/llmcore.py b/llmcore.py index 8e812d5..b280d74 100644 --- a/llmcore.py +++ b/llmcore.py @@ -456,75 +456,6 @@ class LLMSession: if stream: return _ask_gen() 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): @@ -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 = """ ### 行动规范(持续有效) diff --git a/mykey_template.py b/mykey_template.py index 1c12218..1a5a674 100644 --- a/mykey_template.py +++ b/mykey_template.py @@ -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...'