Initial commit: AegisSight-Monitor (OSINT-Monitoringsystem)
Dieser Commit ist enthalten in:
88
src/agents/claude_client.py
Normale Datei
88
src/agents/claude_client.py
Normale Datei
@@ -0,0 +1,88 @@
|
||||
"""Shared Claude CLI Client mit Usage-Tracking."""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from config import CLAUDE_PATH, CLAUDE_TIMEOUT
|
||||
|
||||
logger = logging.getLogger("osint.claude_client")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClaudeUsage:
|
||||
"""Token-Verbrauch eines einzelnen Claude CLI Aufrufs."""
|
||||
input_tokens: int = 0
|
||||
output_tokens: int = 0
|
||||
cache_creation_tokens: int = 0
|
||||
cache_read_tokens: int = 0
|
||||
cost_usd: float = 0.0
|
||||
duration_ms: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class UsageAccumulator:
|
||||
"""Akkumuliert Usage über mehrere Claude-Aufrufe eines Refreshs."""
|
||||
input_tokens: int = 0
|
||||
output_tokens: int = 0
|
||||
cache_creation_tokens: int = 0
|
||||
cache_read_tokens: int = 0
|
||||
total_cost_usd: float = 0.0
|
||||
call_count: int = 0
|
||||
|
||||
def add(self, usage: ClaudeUsage):
|
||||
self.input_tokens += usage.input_tokens
|
||||
self.output_tokens += usage.output_tokens
|
||||
self.cache_creation_tokens += usage.cache_creation_tokens
|
||||
self.cache_read_tokens += usage.cache_read_tokens
|
||||
self.total_cost_usd += usage.cost_usd
|
||||
self.call_count += 1
|
||||
|
||||
|
||||
async def call_claude(prompt: str, tools: str | None = "WebSearch,WebFetch") -> tuple[str, ClaudeUsage]:
|
||||
"""Ruft Claude CLI auf. Gibt (result_text, usage) zurück."""
|
||||
cmd = [CLAUDE_PATH, "-p", prompt, "--output-format", "json"]
|
||||
if tools:
|
||||
cmd.extend(["--allowedTools", tools])
|
||||
else:
|
||||
cmd.extend(["--max-turns", "1"])
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
|
||||
env={"PATH": "/usr/local/bin:/usr/bin:/bin", "HOME": "/home/claude-dev"},
|
||||
)
|
||||
try:
|
||||
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=CLAUDE_TIMEOUT)
|
||||
except asyncio.TimeoutError:
|
||||
process.kill()
|
||||
raise TimeoutError(f"Claude CLI Timeout nach {CLAUDE_TIMEOUT}s")
|
||||
|
||||
if process.returncode != 0:
|
||||
error_msg = stderr.decode("utf-8", errors="replace").strip()
|
||||
logger.error(f"Claude CLI Fehler (Exit {process.returncode}): {error_msg}")
|
||||
raise RuntimeError(f"Claude CLI Fehler: {error_msg}")
|
||||
|
||||
raw = stdout.decode("utf-8", errors="replace").strip()
|
||||
usage = ClaudeUsage()
|
||||
result_text = raw
|
||||
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
result_text = data.get("result", raw)
|
||||
u = data.get("usage", {})
|
||||
usage = ClaudeUsage(
|
||||
input_tokens=u.get("input_tokens", 0),
|
||||
output_tokens=u.get("output_tokens", 0),
|
||||
cache_creation_tokens=u.get("cache_creation_input_tokens", 0),
|
||||
cache_read_tokens=u.get("cache_read_input_tokens", 0),
|
||||
cost_usd=data.get("total_cost_usd", 0.0),
|
||||
duration_ms=data.get("duration_ms", 0),
|
||||
)
|
||||
logger.info(
|
||||
f"Claude: {usage.input_tokens} in / {usage.output_tokens} out / "
|
||||
f"cache {usage.cache_creation_tokens}+{usage.cache_read_tokens} / "
|
||||
f"${usage.cost_usd:.4f} / {usage.duration_ms}ms"
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("Claude CLI Antwort kein gültiges JSON, nutze raw output")
|
||||
|
||||
return result_text, usage
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren