|
|
@@ -1,5 +1,5 @@
|
|
|
"""
|
|
|
-agent67.core.claude_lam — 基于 Claude Code CLI 的 Lam 实现
|
|
|
+agent67.core.claude_lam — 基于 Claude Code CLI 的 Lam 实现 (v2 流式输出)
|
|
|
|
|
|
不需要 API Key,直接使用 Claude Code Max Plan。
|
|
|
|
|
|
@@ -7,18 +7,16 @@ Lambda 语义不变:
|
|
|
ClaudeLam("name", "prompt") ≡ λ_D . F_{claude,D}
|
|
|
调用 = β-规约 = claude -p 解码
|
|
|
|
|
|
-用法:
|
|
|
- brain = ClaudeLam("assistant", "你是一个助手")
|
|
|
- result = brain("帮我查看文件", ctx)
|
|
|
+v2: 流式输出 — 用户能实时看到 LLM 的回复过程
|
|
|
"""
|
|
|
from __future__ import annotations
|
|
|
|
|
|
-import json
|
|
|
import subprocess
|
|
|
+import sys
|
|
|
+import threading
|
|
|
import time
|
|
|
from typing import Any, Callable, Optional
|
|
|
|
|
|
-import sys
|
|
|
from pathlib import Path
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent
|
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
@@ -28,10 +26,7 @@ from lambdagent.core import Term, Context
|
|
|
|
|
|
class ClaudeLam(Term):
|
|
|
"""
|
|
|
- 基于 Claude Code CLI 的 Lambda 抽象。
|
|
|
-
|
|
|
- 等价于 Lam,但通过 `claude -p` 调用,
|
|
|
- 使用 Claude Code Max Plan 的额度,无需 API Key。
|
|
|
+ 基于 Claude Code CLI 的 Lambda 抽象(流式输出版)。
|
|
|
|
|
|
Lambda: ClaudeLam("name", "prompt") = λx. claude(prompt, x)
|
|
|
"""
|
|
|
@@ -44,6 +39,7 @@ class ClaudeLam(Term):
|
|
|
max_tokens: int = 4096,
|
|
|
output_parser: Callable[[str], Any] | None = None,
|
|
|
claude_bin: str = "claude",
|
|
|
+ stream: bool = True,
|
|
|
):
|
|
|
super().__init__(name)
|
|
|
self.prompt = prompt
|
|
|
@@ -51,43 +47,111 @@ class ClaudeLam(Term):
|
|
|
self.max_tokens = max_tokens
|
|
|
self.output_parser = output_parser or (lambda x: x)
|
|
|
self.claude_bin = claude_bin
|
|
|
+ self.stream = stream
|
|
|
|
|
|
def apply(self, input: Any, ctx: Context | None = None) -> Any:
|
|
|
"""β-规约: (λ_D x) → claude -p (prompt + x)"""
|
|
|
ctx = ctx or Context()
|
|
|
t0 = time.time()
|
|
|
|
|
|
- raw = self._call_claude(str(input))
|
|
|
+ if self.stream:
|
|
|
+ raw = self._call_claude_stream(str(input))
|
|
|
+ else:
|
|
|
+ raw = self._call_claude(str(input))
|
|
|
+
|
|
|
duration = (time.time() - t0) * 1000
|
|
|
result = self.output_parser(raw)
|
|
|
-
|
|
|
ctx.log(self._name, self._trace_id, input, result, duration, f"claude-code/{self.model}")
|
|
|
return result
|
|
|
|
|
|
- def _call_claude(self, input_text: str) -> str:
|
|
|
+ def _call_claude_stream(self, input_text: str) -> str:
|
|
|
"""
|
|
|
- 通过 claude CLI 执行 β-规约。
|
|
|
-
|
|
|
- 等价于: echo "input" | claude -p --system-prompt "prompt" --output-format text
|
|
|
+ 流式调用 claude CLI — 实时显示输出。
|
|
|
+ 使用 Popen 逐字符读取 stdout。
|
|
|
"""
|
|
|
- # 将 system prompt 和用户输入合并,减少 CLI 参数长度
|
|
|
- # 超长 system prompt 通过 stdin 传入更可靠
|
|
|
full_input = f"[System Instructions]\n{self.prompt}\n\n[User Input]\n{input_text}"
|
|
|
|
|
|
try:
|
|
|
cmd = [
|
|
|
self.claude_bin,
|
|
|
- "-p", # print mode (non-interactive)
|
|
|
- "--output-format", "text", # 纯文本输出
|
|
|
- "--model", self.model, # 模型选择
|
|
|
+ "-p",
|
|
|
+ "--output-format", "text",
|
|
|
+ "--model", self.model,
|
|
|
]
|
|
|
|
|
|
- result = subprocess.run(
|
|
|
+ proc = subprocess.Popen(
|
|
|
cmd,
|
|
|
- input=full_input,
|
|
|
- capture_output=True,
|
|
|
+ stdin=subprocess.PIPE,
|
|
|
+ stdout=subprocess.PIPE,
|
|
|
+ stderr=subprocess.PIPE,
|
|
|
text=True,
|
|
|
- timeout=180, # 3 分钟超时(复杂任务需要更多时间)
|
|
|
+ bufsize=1,
|
|
|
+ )
|
|
|
+
|
|
|
+ # 写入 stdin 并关闭(触发处理)
|
|
|
+ proc.stdin.write(full_input)
|
|
|
+ proc.stdin.close()
|
|
|
+
|
|
|
+ # 流式读取 stdout
|
|
|
+ output_chars = []
|
|
|
+ start = time.time()
|
|
|
+ first_token = True
|
|
|
+
|
|
|
+ while True:
|
|
|
+ char = proc.stdout.read(1)
|
|
|
+ if not char:
|
|
|
+ break
|
|
|
+ if time.time() - start > 180:
|
|
|
+ proc.kill()
|
|
|
+ return "[Claude Code 超时] 请求超过 180 秒"
|
|
|
+
|
|
|
+ output_chars.append(char)
|
|
|
+
|
|
|
+ # 实时显示(淡色,和最终回复区分)
|
|
|
+ if first_token:
|
|
|
+ sys.stdout.write(" 🐑 ")
|
|
|
+ first_token = False
|
|
|
+ sys.stdout.write(char)
|
|
|
+ sys.stdout.flush()
|
|
|
+
|
|
|
+ # 等待进程结束
|
|
|
+ proc.wait(timeout=5)
|
|
|
+
|
|
|
+ if not first_token:
|
|
|
+ sys.stdout.write("\n")
|
|
|
+ sys.stdout.flush()
|
|
|
+
|
|
|
+ if proc.returncode != 0:
|
|
|
+ stderr = proc.stderr.read().strip()
|
|
|
+ if stderr:
|
|
|
+ return f"[Claude Code 错误] {stderr[:500]}"
|
|
|
+
|
|
|
+ output = "".join(output_chars).strip()
|
|
|
+ return output if output else "[无输出]"
|
|
|
+
|
|
|
+ except FileNotFoundError:
|
|
|
+ return (
|
|
|
+ "[错误] 找不到 claude 命令。"
|
|
|
+ "请确保已安装 Claude Code: npm install -g @anthropic-ai/claude-code"
|
|
|
+ )
|
|
|
+ except Exception as e:
|
|
|
+ return f"[错误] {e}"
|
|
|
+
|
|
|
+ def _call_claude(self, input_text: str) -> str:
|
|
|
+ """非流式调用(fallback)。"""
|
|
|
+ full_input = f"[System Instructions]\n{self.prompt}\n\n[User Input]\n{input_text}"
|
|
|
+
|
|
|
+ try:
|
|
|
+ cmd = [
|
|
|
+ self.claude_bin,
|
|
|
+ "-p",
|
|
|
+ "--output-format", "text",
|
|
|
+ "--model", self.model,
|
|
|
+ ]
|
|
|
+
|
|
|
+ result = subprocess.run(
|
|
|
+ cmd, input=full_input,
|
|
|
+ capture_output=True, text=True, timeout=180,
|
|
|
)
|
|
|
|
|
|
if result.returncode != 0:
|
|
|
@@ -102,14 +166,10 @@ class ClaudeLam(Term):
|
|
|
except subprocess.TimeoutExpired:
|
|
|
return "[Claude Code 超时] 请求超过 180 秒"
|
|
|
except FileNotFoundError:
|
|
|
- return (
|
|
|
- "[错误] 找不到 claude 命令。"
|
|
|
- "请确保已安装 Claude Code: npm install -g @anthropic-ai/claude-code"
|
|
|
- )
|
|
|
+ return "[错误] 找不到 claude 命令。"
|
|
|
except Exception as e:
|
|
|
return f"[错误] {e}"
|
|
|
|
|
|
def __rshift__(self, other):
|
|
|
- """支持 >> 组合"""
|
|
|
from lambdagent.primitives import Compose
|
|
|
return Compose(self, other)
|