aborist/qa/ implements the cache-first answer flow from the providence
whitepaper, scaled up to v9.8's full 8-dim admissibility invariant.
cache_key = SHA-256 of:
source_root | question_hash | model_profile_hash | conversation_hash
| governance_policy_hash | schema_version | canonicalization_version
| chunking_version
Any drift in any dimension yields a distinct cache_key — prior records
cannot serve. Falsification states (failed/stale/quarantined) gate
every cache hit.
- qa/keys.py — pure hash functions, deterministic & testable
- qa/client.py — ChatClient Protocol + StubClient + OpenAI-compatible
HTTP client (vllm/llama.cpp/uncloseai compatible)
- qa/runner.py — ask(): lookup -> hit (no LLM call, hit_count++)
OR miss (call client, write record, audit event,
proof binds answer to source root)
- cli.py — `aborist ask` and `aborist providence` subcommands
- pyproject.toml — httpx promoted from extras to core (used by both
html and qa); selectolax stays in [html] extras
Smoke (StubClient, no network): cache miss writes record with chunk_0
Merkle proof reconstructing source_root; cache hit returns same record
without calling client; 1085 audit events chained 0 breaks across
ingest/derive/evict/rehydrate/providence_write.
91 lines
2.6 KiB
Python
91 lines
2.6 KiB
Python
"""Chat-completion clients.
|
|
|
|
ChatClient is a Protocol — any object with a `chat_completion` method
|
|
plugs in. We ship two concrete clients:
|
|
|
|
- OpenAICompatibleClient — talks to any OpenAI-compatible /v1/chat/completions
|
|
endpoint (vllm, llama.cpp server, ollama, TGI, hosted services).
|
|
- StubClient — offline canned responses for tests and dry-runs. No
|
|
network. Operation Voyeur safe.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Protocol
|
|
|
|
|
|
class ChatClient(Protocol):
|
|
def chat_completion(
|
|
self,
|
|
messages: list[dict],
|
|
*,
|
|
model: str,
|
|
temperature: float = 0.1,
|
|
max_tokens: int = 512,
|
|
top_p: float = 1.0,
|
|
) -> str:
|
|
"""Return the assistant's text response."""
|
|
...
|
|
|
|
|
|
class StubClient:
|
|
"""Offline client for tests / --dry-run.
|
|
|
|
Pass `answer=callable(messages, **kw) -> str` for dynamic stubbing.
|
|
"""
|
|
|
|
def __init__(self, answer="[STUB] dry-run answer; no LLM was called."):
|
|
self._answer = answer
|
|
self.calls: list[dict] = []
|
|
|
|
def chat_completion(self, messages, **kwargs) -> str:
|
|
self.calls.append({"messages": messages, "kwargs": kwargs})
|
|
if callable(self._answer):
|
|
return self._answer(messages, **kwargs)
|
|
return self._answer
|
|
|
|
|
|
class OpenAICompatibleClient:
|
|
"""OpenAI-compatible chat completion over HTTP.
|
|
|
|
Default endpoint is configurable via env. Pass api_key only if the
|
|
target requires it; uncloseai's free endpoint does not.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
base_url: str,
|
|
api_key: str | None = None,
|
|
timeout: float = 60.0,
|
|
):
|
|
self.base_url = base_url.rstrip("/")
|
|
self.api_key = api_key
|
|
self.timeout = timeout
|
|
|
|
def chat_completion(
|
|
self,
|
|
messages: list[dict],
|
|
*,
|
|
model: str,
|
|
temperature: float = 0.1,
|
|
max_tokens: int = 512,
|
|
top_p: float = 1.0,
|
|
) -> str:
|
|
import httpx
|
|
|
|
headers = {"Content-Type": "application/json"}
|
|
if self.api_key:
|
|
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
payload = {
|
|
"model": model,
|
|
"messages": messages,
|
|
"temperature": temperature,
|
|
"max_tokens": max_tokens,
|
|
"top_p": top_p,
|
|
}
|
|
url = f"{self.base_url}/chat/completions"
|
|
with httpx.Client(timeout=self.timeout) as client:
|
|
resp = client.post(url, headers=headers, json=payload)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
return data["choices"][0]["message"]["content"]
|