Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
8.7 kB · 253 lines
Rust
at dev
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254//! The medium loop's interpreter layer (docs/04). Interpreter LLMs sit behind//! the `Glosser` trait; the ensemble is a Vec of them and disagreement is//! *kept* — it is the cheapest available measurement of interpreter//! contamination. Never called in the request path.
pub mod canon;pub mod embed;pub mod worker;
use async_trait::async_trait;use serde::Deserialize;
pub const PROMPT_VERSION: &str = "gloss-v1";
/// One relational reading proposed by an interpreter.#[derive(Debug, Clone)]pub struct ProposedReading { /// Free-text gloss of the relation R the completer abduced between a:b (and applied to c:d). pub relation: String, pub weight: f64, /// Valence of the relation in [-1, 1]: guarantor vs. threat, kinship vs. antagonism. pub valence: f64,}
#[derive(Debug, Clone)]pub struct GlossOutput { pub readings: Vec<ProposedReading>, pub model: String, pub temperature: Option<f64>,}
/// The square as shown plus the user's response, for interpretation.#[derive(Debug, Clone)]pub struct GlossInput { pub a: Option<String>, pub b: Option<String>, pub c: Option<String>, pub d: Option<String>, pub open_slot: String, pub text: Option<String>, pub mode: String,}
#[async_trait]pub trait Glosser: Send + Sync { fn id(&self) -> String; async fn gloss(&self, input: &GlossInput) -> anyhow::Result<GlossOutput>;}
pub type Ensemble = Vec<Box<dyn Glosser>>;
/// Build the ensemble from config: at least two members whenever an API key is/// present (two models, or one model at two temperatures); the deterministic/// heuristic member otherwise, so the pipeline runs without credentials.pub fn build_ensemble(config: &crate::config::Config) -> Ensemble { match &config.anthropic_api_key { Some(key) => { let mut members: Ensemble = Vec::new(); for model in &config.gloss_models { members.push(Box::new(AnthropicGlosser { api_key: key.clone(), model: model.clone(), temperature: 0.3, http: reqwest::Client::new(), })); } if members.len() < 2 { // Temperature ensemble fallback (docs/04): same family, spread kept. if let Some(model) = config.gloss_models.first() { members.push(Box::new(AnthropicGlosser { api_key: key.clone(), model: model.clone(), temperature: 1.0, http: reqwest::Client::new(), })); } } tracing::info!("gloss ensemble: {} LLM members", members.len()); members } None => { tracing::warn!( "no ANTHROPIC_API_KEY; gloss ensemble is the deterministic heuristic \ interpreter (readings will be structural, not semantic)" ); vec![Box::new(HeuristicGlosser)] } }}
// ============ Anthropic implementation ============
pub struct AnthropicGlosser { api_key: String, model: String, temperature: f64, http: reqwest::Client,}
#[derive(Deserialize)]struct AnthropicResponse { content: Vec<AnthropicContent>,}
#[derive(Deserialize)]struct AnthropicContent { #[serde(default)] text: String,}
#[derive(Deserialize)]struct ReadingJson { relation: String, weight: f64, #[serde(default)] valence: f64,}
const GLOSS_SYSTEM: &str = "You interpret completions of analogical squares (a : b :: c : d). \The datum is not the surface token d but the relation R the completer had to abduce between a and b \and then apply to c. Respond with a JSON array of 1-4 candidate readings, each \{\"relation\": \"<concise gloss of R, 3-12 words, e.g. 'is institutionalized by', 'is threatened by', 'is the sacred source of'>\", \\"weight\": <0..1, your credence in this reading>, \\"valence\": <-1..1, whether R binds its terms in kinship (+) or antagonism (-)>}. \Weights need not sum to 1. If the response was a refusal, joke, or 'does not compute', read THAT: \gloss what relation the square presupposed that the respondent's frame rejected. \Return ONLY the JSON array.";
#[async_trait]impl Glosser for AnthropicGlosser { fn id(&self) -> String { format!("{}@t{}", self.model, self.temperature) }
async fn gloss(&self, input: &GlossInput) -> anyhow::Result<GlossOutput> { let fmt = |x: &Option<String>, slot: &str| { if input.open_slot == slot { input.text.clone().unwrap_or_else(|| "___".into()) } else { x.clone().unwrap_or_else(|| "?".into()) } }; let user_msg = format!( "Square: {} : {} :: {} : {}\nOpen slot: {}\nResponse mode: {}\nResponse text: {}", fmt(&input.a, "a"), fmt(&input.b, "b"), fmt(&input.c, "c"), fmt(&input.d, "d"), input.open_slot, input.mode, input.text.as_deref().unwrap_or("(none)"), );
let body = serde_json::json!({ "model": self.model, "max_tokens": 1024, "temperature": self.temperature, "system": GLOSS_SYSTEM, "messages": [{"role": "user", "content": user_msg}], }); let resp = self .http .post("https://api.anthropic.com/v1/messages") .header("x-api-key", &self.api_key) .header("anthropic-version", "2023-06-01") .json(&body) .send() .await? .error_for_status()? .json::<AnthropicResponse>() .await?;
let text = resp .content .first() .map(|c| c.text.clone()) .unwrap_or_default(); let json_str = text .find('[') .and_then(|start| text.rfind(']').map(|end| &text[start..=end])) .ok_or_else(|| anyhow::anyhow!("no JSON array in gloss response"))?; let parsed: Vec<ReadingJson> = serde_json::from_str(json_str)?;
Ok(GlossOutput { readings: parsed .into_iter() .take(8) .map(|r| ProposedReading { relation: r.relation, weight: r.weight.clamp(0.0, 1.0), valence: r.valence.clamp(-1.0, 1.0), }) .collect(), model: self.model.clone(), temperature: Some(self.temperature), }) }}
// ============ deterministic fallback ============
/// Structural (non-semantic) interpreter used when no LLM is configured. Its/// glosses describe the completion's form, not its meaning — enough to exercise/// canonicalization, the term graph, and the renderer with honest provenance/// (interpreter id "heuristic-v0" is visible in every gloss record).pub struct HeuristicGlosser;
#[async_trait]impl Glosser for HeuristicGlosser { fn id(&self) -> String { "heuristic-v0".into() }
async fn gloss(&self, input: &GlossInput) -> anyhow::Result<GlossOutput> { let readings = match input.mode.as_str() { "refused" => vec![ProposedReading { relation: "relation rejected as unaskable".into(), weight: 1.0, valence: -0.5, }], "noparse" => vec![ProposedReading { relation: "relation failed to parse".into(), weight: 1.0, valence: 0.0, }], "joke" => vec![ProposedReading { relation: "relation deflected by irony".into(), weight: 1.0, valence: 0.3, }], _ => { // Distinctive content only — shared scaffolding words would make // the trigram embedder cluster every gloss into one relation. let pair = |x: &Option<String>, y: &Option<String>| { format!("{} ↔ {}", x.as_deref().unwrap_or("?"), y.as_deref().unwrap_or("?")) }; vec![ProposedReading { relation: match input.open_slot.as_str() { "a" | "b" => pair(&input.c, &input.d), _ => pair(&input.a, &input.b), }, weight: 0.6, valence: 0.0, }] } }; Ok(GlossOutput { readings, model: "heuristic-v0".into(), temperature: None, }) }}