agent_harness -> rein_aharness (package + all imports), CLI command agent-harness -> rein-aharness, Docker image tag, k8s namespace/labels/ names, Makefile targets, deploy script env var/paths. In-repo identity strings (hub event source, metrics harness field, default assignee, argparse prog name, commit author identity) updated to match. Historical documents left untouched on purpose: docs/adr/ADR-001-agent-harness-architecture.md, docs/architecture.md (dated v0.1 snapshot), workplans/HARNESS-WP-0001 (completed under the old name), and the SSH host alias "forgejo-agent-harness" (external ~/.ssh/config entry, not owned here). Verified: 47/47 tests pass, CLI runs correctly from a fresh venv, `make image` builds and the resulting container runs correctly. deploy/README.md gained an explicit rename cutover checklist for what this session cannot safely do unattended -- moving the host-side secrets dir and checkout on railiance01, and not deleting the old k8s namespace until the new one is confirmed working. The actual live cutover (running that checklist against the real Railiance deployment) is not attempted here -- real production surgery on binky-control's live automation, needs the operator present. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
103 lines
3 KiB
Python
103 lines
3 KiB
Python
"""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; rein-aharness 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
|