feat: mail-triage via llm-connect (no host Claude)
Add LLMConnectClient and mail-triage: CSV metadata → OpenRouter-backed llm-connect JSON plan → deterministic mail-log apply + commit. Server path for Binky mail intake on Railiance (BINKY-WP-0006).
This commit is contained in:
parent
66ccd4fa01
commit
7520a53831
7 changed files with 730 additions and 4 deletions
103
agent_harness/llm_connect_client.py
Normal file
103
agent_harness/llm_connect_client.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""HTTP client for in-cluster / remote llm-connect (server mode).
|
||||
|
||||
Mirrors activity-core's llm_client pattern: provider keys and model routing
|
||||
stay behind llm-connect; agent-harness only sends prompts over HTTP.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
_SAFE_RESPONSE_METADATA_KEYS = frozenset(
|
||||
{
|
||||
"finish_reason",
|
||||
"usage",
|
||||
"model",
|
||||
"model_name",
|
||||
"provider",
|
||||
"request_id",
|
||||
"response_id",
|
||||
"trace_id",
|
||||
"latency_ms",
|
||||
"duration_ms",
|
||||
"elapsed_ms",
|
||||
"created",
|
||||
"created_at",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class LLMConnectError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class LLMConnectClient:
|
||||
"""Synchronous client for llm-connect ``POST /execute``."""
|
||||
|
||||
def __init__(self, base_url: str, timeout_seconds: float = 300.0) -> None:
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.timeout_seconds = timeout_seconds
|
||||
self.last_response_metadata: dict[str, Any] | None = None
|
||||
|
||||
def complete(
|
||||
self,
|
||||
prompt: str,
|
||||
*,
|
||||
model: str = "",
|
||||
config: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
run_config = dict(config or {})
|
||||
if model and "model_name" not in run_config:
|
||||
run_config["model_name"] = model
|
||||
run_config.setdefault("timeout_seconds", int(self.timeout_seconds))
|
||||
payload: dict[str, Any] = {"prompt": prompt, "config": run_config}
|
||||
try:
|
||||
resp = httpx.post(
|
||||
f"{self.base_url}/execute",
|
||||
json=payload,
|
||||
timeout=self.timeout_seconds,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
except httpx.HTTPError as exc:
|
||||
raise LLMConnectError(f"llm-connect request failed: {exc}") from exc
|
||||
try:
|
||||
data = resp.json()
|
||||
except ValueError as exc:
|
||||
raise LLMConnectError("llm-connect returned non-JSON body") from exc
|
||||
self.last_response_metadata = _extract_response_metadata(data)
|
||||
content = data.get("content")
|
||||
if not isinstance(content, str) or not content.strip():
|
||||
raise LLMConnectError("llm-connect response missing string content")
|
||||
return content
|
||||
|
||||
|
||||
def get_llm_connect_client() -> LLMConnectClient:
|
||||
base_url = os.environ.get("LLM_CONNECT_URL", "").strip()
|
||||
if not base_url:
|
||||
raise LLMConnectError(
|
||||
"LLM_CONNECT_URL is not set "
|
||||
"(e.g. http://llm-connect.activity-core.svc.cluster.local:8080)"
|
||||
)
|
||||
timeout = float(os.environ.get("LLM_CONNECT_TIMEOUT_SECONDS", "300"))
|
||||
return LLMConnectClient(base_url, timeout)
|
||||
|
||||
|
||||
def _extract_response_metadata(data: dict[str, Any]) -> dict[str, Any]:
|
||||
out: dict[str, Any] = {}
|
||||
for key, value in data.items():
|
||||
if key in _SAFE_RESPONSE_METADATA_KEYS and _json_safe(value):
|
||||
out[key] = value
|
||||
return out
|
||||
|
||||
|
||||
def _json_safe(value: Any) -> bool:
|
||||
try:
|
||||
import json
|
||||
|
||||
json.dumps(value)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return True
|
||||
Loading…
Add table
Add a link
Reference in a new issue