Files
GenericAgent/agentmain.py
T
dd3xp b1352f2de1 chore: sync framework to upstream lsdefine/GenericAgent @ b365945
上次同步点是 1eb8c4c (我们 fork 上为 8f89766),lsdefine 自那以来 102 个

commit,包括飞书 IM 插件、TUI v2 修、conductor OOP 重构、L4 scheduler

mkdir 修复、checklist 系统、computer_use SOP 等。

处理分桶:

  - 45 个 lsdefine-only 文件:整文件覆盖(safe,无我方改动)

  - 17 个 ours-only 文件:完全保留(桌面前端 + bridge + plan_state)

  - 4 个双方都动:逐个 3-way 合

      * .gitignore: 取并集(lsdefine 新增 + 我们 /BUG_*.md)

      * reflect/scheduler.py: 已字节对齐 lsdefine,无操作

      * frontends/conductor.py: lsdefine OOP 重构作 base,我们 6 处

          改动(CORS / add_chat files+images / cost_tracker / token-stats

          / WS running / WS receive files+images)原样塞进新结构

      * frontends/conductor.html: lsdefine 重组作 base,我们 40 行

          输入字数限制 + toast 原样塞进 keydown 监听之后

未触动 desktop_bridge.py(memory 规则约定 exclude bridge)。

已 grep 全文件无 <<<<<< / ======= / >>>>>> 冲突标记残留。

我们改动一行未漏,全部按原代码原文落实,没有自创新代码。
2026-06-06 23:05:59 +08:00

305 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os, sys, threading, queue, time, json, re, random, locale
os.environ.setdefault('GA_LANG', 'zh' if any(k in (locale.getlocale()[0] or '').lower() for k in ('zh', 'chinese')) else 'en')
if sys.stdout is None: sys.stdout = open(os.devnull, "w")
elif hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(errors='replace')
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 reload_mykeys, ToolClient, MixinSession, NativeToolClient, NativeClaudeSession, NativeOAISession, resolve_client
from agent_loop import agent_runner_loop
try:
from plugins.hooks import discover_and_load; discover_and_load()
except Exception: pass
from ga import GenericAgentHandler, smart_format, get_global_memory, format_error, consume_file
script_dir = os.path.dirname(os.path.abspath(__file__))
def load_tool_schema(suffix=''):
global TOOLS_SCHEMA
TS = open(os.path.join(script_dir, f'assets/tools_schema{suffix}.json'), 'r', encoding='utf-8').read()
TOOLS_SCHEMA = json.loads(TS if os.name == 'nt' else TS.replace('powershell', 'bash'))
load_tool_schema()
lang_suffix = '_en' if os.environ.get('GA_LANG', '') == 'en' else ''
mem_dir = os.path.join(script_dir, 'memory')
if not os.path.exists(mem_dir): os.makedirs(mem_dir)
mem_txt = os.path.join(mem_dir, 'global_mem.txt')
if not os.path.exists(mem_txt): open(mem_txt, 'w', encoding='utf-8').write('# [Global Memory - L2]\n')
mem_insight = os.path.join(mem_dir, 'global_mem_insight.txt')
if not os.path.exists(mem_insight):
t = os.path.join(script_dir, f'assets/global_mem_insight_template{lang_suffix}.txt')
open(mem_insight, 'w', encoding='utf-8').write(open(t, encoding='utf-8').read() if os.path.exists(t) else '')
cdp_cfg = os.path.join(script_dir, 'assets/tmwd_cdp_bridge/config.js')
if not os.path.exists(cdp_cfg):
try:
os.makedirs(os.path.dirname(cdp_cfg), exist_ok=True)
open(cdp_cfg, 'w', encoding='utf-8').write(f"const TID = '__ljq_{hex(random.randint(0, 99999999))[2:8]}';")
except Exception as e: print(f'[WARN] CDP config init failed: {e} — advanced web features (tmwebdriver) will be unavailable.')
def get_system_prompt():
with open(os.path.join(script_dir, f'assets/sys_prompt{lang_suffix}.txt'), 'r', encoding='utf-8') as f: prompt = f.read()
prompt += f"\nToday: {time.strftime('%Y-%m-%d %a')}\n"
prompt += get_global_memory()
return prompt
class GenericAgent:
def __init__(self):
os.makedirs(os.path.join(script_dir, 'temp'), exist_ok=True)
self.lock = threading.Lock()
self.task_dir = None
self.history = []; self.handler = None;
self.task_queue = queue.Queue()
self.is_running = False; self.stop_sig = False; self.llm_no = 0;
self.inc_out = False; self.verbose = True; self.show_mode = 'text'
self.peer_hint = True
self.force_non_stream = False
logid = f'{(time.time_ns() + random.randrange(1_000_000)) % 1_000_000:06d}'
self.log_path = os.path.join(script_dir, f'temp/model_responses/model_responses_{logid}.txt')
self.load_llm_sessions()
def load_llm_sessions(self):
mykeys, changed = reload_mykeys()
if not changed and hasattr(self, 'llmclients'): return
try: oldhistory = self.llmclient.backend.history
except: oldhistory = None
llm_sessions = []
for k, cfg in mykeys.items():
if not any(x in k for x in ['api', 'config', 'cookie']): continue
try:
if 'mixin' in k: llm_sessions += [{'mixin_cfg': cfg}]
elif c := resolve_client(k): llm_sessions += [c]
except: pass
for i, s in enumerate(llm_sessions):
if isinstance(s, dict) and 'mixin_cfg' in s:
try:
mixin = MixinSession(llm_sessions, s['mixin_cfg'])
if isinstance(mixin._sessions[0], (NativeClaudeSession, NativeOAISession)): llm_sessions[i] = NativeToolClient(mixin)
else: llm_sessions[i] = ToolClient(mixin)
except Exception as e: print(f'\n\n\n[ERROR] Failed to init MixinSession with cfg {s["mixin_cfg"]}: {e}!!!\n\n')
self.llmclients = llm_sessions
self.llmclient = self.llmclients[self.llm_no%len(self.llmclients)]
if oldhistory: self.llmclient.backend.history = oldhistory
def next_llm(self, n=-1):
self.load_llm_sessions()
self.llm_no = ((self.llm_no + 1) if n < 0 else n) % len(self.llmclients)
lastc = self.llmclient
self.llmclient = self.llmclients[self.llm_no]
try: self.llmclient.backend.history = lastc.backend.history
except: raise Exception('[ERROR] BAD Mixin config: Check your mykey.py')
self.llmclient.last_tools = ''
name = self.get_llm_name(model=True)
if 'glm' in name or 'minimax' in name or 'kimi' in name: load_tool_schema('_cn')
else: load_tool_schema()
def list_llms(self):
self.load_llm_sessions()
return [(i, self.get_llm_name(b), i == self.llm_no) for i, b in enumerate(self.llmclients)]
def get_llm_name(self, b=None, model=False):
b = self.llmclient if b is None else b
if isinstance(b, dict): return 'BADCONFIG_MIXIN'
if model: return b.backend.model.lower()
return f"{type(b.backend).__name__}/{b.backend.name}"
def abort(self):
if not self.is_running: return
print('Abort current task...')
self.stop_sig = True
if self.handler is not None: self.handler.code_stop_signal.append(1)
def put_task(self, query, source="user", images=None):
display_queue = queue.Queue()
self.task_queue.put({"query": query, "source": source, "images": images or [], "output": display_queue})
return display_queue
# i know it is dangerous, but raw_query is dangerous enough it doesn't enlarge
def _handle_slash_cmd(self, raw_query, display_queue):
if not raw_query.startswith('/'): return raw_query
if _sm := re.match(r'/session\.(\w+)=(.*)', raw_query.strip()):
k, v = _sm.group(1), _sm.group(2)
vfile = os.path.join(script_dir, 'temp', v)
if os.path.isfile(vfile): v = open(vfile, encoding='utf-8').read().strip()
try: v = json.loads(v) # cover number parsing
except (json.JSONDecodeError, ValueError): pass
setattr(self.llmclient.backend, k, v)
display_queue.put({'done': smart_format(f"✅ session.{k} = {repr(v)}", max_str_len=500), 'source': 'system'})
return None
if raw_query.strip() == '/resume':
return r'帮我看看最近有哪些会话可以恢复。读model_responses/目录,按修改时间取最近10个文件,从每个文件里找最后一个<history>...</history>块,用一句话总结每个会话在聊什么,列表给我选。注意读文件后要把字面的\n替换成真换行才能正确匹配。'
return raw_query
def run(self):
while True:
task = self.task_queue.get()
if isinstance(task, str): break
raw_query, source, display_queue = task["query"], task["source"], task["output"]
raw_query = self._handle_slash_cmd(raw_query, display_queue)
if raw_query is None:
self.task_queue.task_done(); continue
self.is_running = True
if len(raw_query) > 1500:
task_file = os.path.join(script_dir, 'temp', f'user_prompt_{int(time.time())}.md')
with open(task_file, 'w', encoding='utf-8') as f: f.write(raw_query)
raw_query = f'Long user prompt saved to {task_file}. Read and execute.'
rquery = smart_format(raw_query.replace('\n', ' '), max_str_len=200)
self.history.append(f"[USER]: {rquery}")
sys_prompt = get_system_prompt() + getattr(self.llmclient.backend, 'extra_sys_prompt', '')
if self.peer_hint: sys_prompt += f"\n[Peer] 用户提及其他会话/后台任务状态时: temp/model_responses/ (只找近期修改的文件尾部)\n"
handler = GenericAgentHandler(self, self.history, os.path.join(script_dir, 'temp'))
if getattr(self, 'no_print', False): handler.print = lambda *a, **k: None
if self.handler and 'key_info' in self.handler.working:
ki = re.sub(r'\n\[SYSTEM\] 此为.*?工作记忆[。\n]*', '', self.handler.working['key_info']) # 去旧
handler.working['key_info'] = ki
handler.working['passed_sessions'] = ps = self.handler.working.get('passed_sessions', 0) + 1
if ps > 0: handler.working['key_info'] += f'\n[SYSTEM] 此为 {ps} 个对话前设置的key_info,若已在新任务,先更新或清除工作记忆。\n'
self.handler = handler # although new handler, the **full** history is in llmclient, so it is full history!
self.llmclient.log_path = self.log_path
if self.force_non_stream:
self.llmclient.backend.stream = False
self.llmclient.backend.read_timeout = max(self.llmclient.backend.read_timeout, 1200)
gen = agent_runner_loop(self.llmclient, sys_prompt, raw_query, handler, TOOLS_SCHEMA,
max_turns=80, verbose=self.verbose, yield_info=True)
try:
full_resp = ""; last_pos = 0; curr_turn = 0; turn_resps = []
for chunk in gen:
if consume_file(self.task_dir, '_stop'): self.abort()
if self.stop_sig: break
if isinstance(chunk, dict) and 'turn' in chunk:
curr_turn = chunk['turn']; turn_resps.append(''); continue
full_resp += chunk; turn_resps[-1] += chunk
if len(full_resp) - last_pos > 30 or 'LLM Running' in chunk:
display_queue.put({'next': full_resp[last_pos:] if self.inc_out else full_resp,
'source': source, 'turn': curr_turn, 'outputs': turn_resps[-2:]})
last_pos = len(full_resp)
if self.inc_out and last_pos < len(full_resp):
display_queue.put({'next': full_resp[last_pos:], 'source': source,
'turn': curr_turn, 'outputs': turn_resps[-2:]})
#if '</summary>' in full_resp: full_resp = full_resp.replace('</summary>', '</summary>\n\n')
#if '</file_content>' in full_resp: full_resp = re.sub(r'<file_content>\s*(.*?)\s*</file_content>', r'\n````\n<file_content>\n\1\n</file_content>\n````', full_resp, flags=re.DOTALL)
display_queue.put({'done': full_resp, 'source': source, 'turn': curr_turn, 'outputs': turn_resps.copy()})
self.history = handler.history_info
except Exception as e:
print(f"Backend Error: {format_error(e)}")
display_queue.put({'done': full_resp + f'\n```\n{format_error(e)}\n```', 'source': source, 'turn': curr_turn, 'outputs': turn_resps.copy()})
finally:
if self.stop_sig: print('User aborted the task.')
self.is_running = self.stop_sig = False
self.task_queue.task_done()
if self.handler is not None: self.handler.code_stop_signal.append(1)
GeneraticAgent = GenericAgent
if __name__ == '__main__':
import argparse
from datetime import datetime
parser = argparse.ArgumentParser()
parser.add_argument('--task', metavar='IODIR', help='一次性任务模式(文件IO)')
parser.add_argument('--reflect', metavar='SCRIPT', help='反射模式:加载监控脚本,check()触发时发任务')
parser.add_argument('--input', help='prompt')
parser.add_argument('--llm_no', type=int, default=0)
parser.add_argument('--verbose', action='store_true')
parser.add_argument('--nobg', action='store_true')
args, _unknown = parser.parse_known_args()
_reflect_args = dict(zip([k.lstrip('-') for k in _unknown[::2]], _unknown[1::2])) if _unknown else {}
if args.task and not args.nobg:
import subprocess, platform
cmd = [sys.executable, os.path.abspath(__file__)] + [a for a in sys.argv[1:]] + ['--nobg']
d = os.path.join(script_dir, f'temp/{args.task}'); os.makedirs(d, exist_ok=True)
p = subprocess.Popen(cmd, cwd=script_dir,
creationflags=0x08000000 if platform.system() == 'Windows' else 0,
stdout=open(os.path.join(d, 'stdout.log'), 'w', encoding='utf-8'),
stderr=open(os.path.join(d, 'stderr.log'), 'w', encoding='utf-8'))
print('PID:', p.pid); sys.exit(0)
agent = GeneraticAgent()
agent.next_llm(args.llm_no)
agent.verbose = args.verbose
threading.Thread(target=agent.run, daemon=True).start()
if args.task:
agent.peer_hint = False
agent.force_non_stream = True
agent.task_dir = d = os.path.join(script_dir, f'temp/{args.task}'); nround = ''
infile = os.path.join(d, 'input.txt')
if args.input:
os.makedirs(d, exist_ok=True)
import glob; [os.remove(f) for f in glob.glob(os.path.join(d, 'output*.txt'))]
with open(infile, 'w', encoding='utf-8') as f: f.write(args.input)
if (fh := consume_file(d, '_history.json')): agent.llmclient.backend.history = json.loads(fh)
with open(infile, encoding='utf-8') as f: raw = f.read()
while True:
dq = agent.put_task(raw, source='task')
while 'done' not in (item := dq.get(timeout=1200)):
if 'next' in item and random.random() < 0.95: # 概率写一次中间结果
with open(f'{d}/output{nround}.txt', 'w', encoding='utf-8') as f: f.write(item.get('next', ''))
with open(f'{d}/output{nround}.txt', 'w', encoding='utf-8') as f: f.write(item['done'] + '\n\n[ROUND END]\n')
consume_file(d, '_stop') # 已经成功停下来了,避免打断下次reply
for _ in range(300): # 等reply.txt,10分钟超时
time.sleep(2)
if (raw := consume_file(d, 'reply.txt')): break
else: break
nround = nround + 1 if isinstance(nround, int) else 1
elif args.reflect:
agent.peer_hint = False
agent.force_non_stream = True
import importlib.util
spec = importlib.util.spec_from_file_location('reflect_script', args.reflect)
mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
if hasattr(mod, 'init'): mod.init(_reflect_args)
_mt = os.path.getmtime(args.reflect)
print(f'[Reflect] loaded {args.reflect}' + (f' args={_reflect_args}' if _reflect_args else ''))
while True:
if os.path.getmtime(args.reflect) != _mt:
try:
spec.loader.exec_module(mod); _mt = os.path.getmtime(args.reflect)
if hasattr(mod, 'init'): mod.init(_reflect_args)
print('[Reflect] reloaded')
except Exception as e: print(f'[Reflect] reload error: {e}')
try: task = mod.check()
except Exception as e:
print(f'[Reflect] check() error: {e}'); task = None
if task and task == '/exit': break
if not task:
time.sleep(getattr(mod, 'INTERVAL', 5)); continue
print(f'[Reflect] triggered: {task[:80]}')
dq = agent.put_task(task, source='reflect')
try:
while 'done' not in (item := dq.get(timeout=1200)): pass
result = item['done']
print(result)
except Exception as e:
if getattr(mod, 'ONCE', False): raise
print(f'[Reflect] drain error: {e}'); result = f'[ERROR] {e}'
log_dir = os.path.join(script_dir, 'temp/reflect_logs'); os.makedirs(log_dir, exist_ok=True)
script_name = os.path.splitext(os.path.basename(args.reflect))[0]
open(os.path.join(log_dir, f'{script_name}_{datetime.now():%Y-%m-%d}.log'), 'a', encoding='utf-8').write(f'[{datetime.now():%m-%d %H:%M}]\n{result}\n\n')
if (on_done := getattr(mod, 'on_done', None)):
try: on_done(result)
except Exception as e: print(f'[Reflect] on_done error: {e}')
if getattr(mod, 'ONCE', False): print('[Reflect] ONCE=True, exiting.'); break
else:
try: import readline
except Exception: pass
agent.inc_out = True
if sys.stdout.isatty():
try: model = agent.get_llm_name(model=True) or '?'
except Exception: model = '?'
try:
sys.stdout.write(f'\x1b[92m✦\x1b[0m \x1b[1mGenericAgent\x1b[0m '
f'\x1b[90m· cli · model:\x1b[0m {model}\n')
sys.stdout.flush()
except Exception: pass
while True:
q = input('> ').strip()
if not q: continue
try:
dq = agent.put_task(q, source='user')
while True:
item = dq.get()
if 'next' in item: print(item['next'], end='', flush=True)
if 'done' in item: print(); break
except KeyboardInterrupt:
agent.abort()
print('\n[Interrupted]')