import type { CommunicationBrief, ObserverOutput } from "./types"; const ENGINE_URL = process.env.IKY_ENGINE_URL?.replace(/\/$/, "") ?? "http://127.0.0.1:7777"; const REQUEST_TIMEOUT_MS = 1500; interface RetrievedItem { item_id: number; creator_id: number | null; title: string | null; category: string | null; score: number; } interface RetrieveResponse { items: RetrievedItem[]; } function hash32(input: string): number { let h = 0x811c9dc5; for (let i = 0; i < input.length; i++) { h ^= input.charCodeAt(i); h = Math.imul(h, 0x01000193); } return (h >>> 0) % 2_147_483_647; } function personToUserId(personId: string): number { return 100_000 + hash32(`person:${personId}`); } function creatorFromDomain(domain: string): number { return 10_000 + hash32(`domain:${domain.toLowerCase()}`); } function itemIdForTurn(conversationId: string, turn: number): number { return 1_000_000 + hash32(`conv:${conversationId}:turn:${turn}`); } async function request(path: string, init: RequestInit = {}): Promise { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); try { return await fetch(`${ENGINE_URL}${path}`, { ...init, headers: { "Content-Type": "application/json", ...(init.headers ?? {}), }, signal: controller.signal, }); } finally { clearTimeout(timer); } } async function postJson(path: string, body: unknown): Promise { try { const res = await request(path, { method: "POST", body: JSON.stringify(body), }); return res.ok; } catch (err) { const msg = err instanceof Error ? err.message : String(err); console.error(`[tidal] POST ${path} failed: ${msg}`); return false; } } export async function ensurePersonalizationUser(personId: string): Promise { const userId = personToUserId(personId); await postJson("/v1/users/upsert", { user_id: userId, metadata: { role: "user", source: "iknowyou", }, }); } export async function ensurePersonalizationSession( conversationId: string, personId: string ): Promise { const userId = personToUserId(personId); await postJson("/v1/sessions/start", { conversation_id: conversationId, user_id: userId, agent_id: "aeries", }); } async function upsertAssistantItem( conversationId: string, turn: number, assistantMessage: string, domain: string ): Promise<{ itemId: number; creatorId: number }> { const itemId = itemIdForTurn(conversationId, turn); const creatorId = creatorFromDomain(domain || "general"); await postJson("/v1/items/upsert", { item_id: itemId, creator_id: creatorId, title: assistantMessage.slice(0, 120), category: domain || "general", }); return { itemId, creatorId }; } function actionFromObserver(output: ObserverOutput): "more" | "less" | "view" { const s = output.engagement.sentiment_score; if (s >= 0.6 && output.engagement.substantive) return "more"; if (s <= 0.4 || output.dynamics.redirected) return "less"; return "view"; } export async function recordObserverPersonalization(params: { personId: string; conversationId: string; turn: number; assistantMessage: string; output: ObserverOutput; }): Promise { const { personId, conversationId, turn, assistantMessage, output } = params; const userId = personToUserId(personId); const domain = output.topic.domain || "general"; const { itemId, creatorId } = await upsertAssistantItem( conversationId, turn, assistantMessage, domain ); const action = actionFromObserver(output); await postJson("/v1/feedback", { user_id: userId, item_id: itemId, creator_id: creatorId, action, }); // Keep sessions warm with signal-level writes for per-conversation context. await postJson("/v1/sessions/signal", { conversation_id: conversationId, signal_type: action === "more" ? "like" : action === "less" ? "skip" : "view", item_id: itemId, weight: 1.0, annotation: `topic:${output.topic.primary} domain:${domain}`, }); // Optional auxiliary memory (Synap when configured in engine server). if (output.engagement.sentiment_score >= 0.75) { await postJson("/v1/aux/observation", { person_id: userId, observation: `Strong positive response to ${domain} / ${output.topic.primary} conversation style`, }); } } export async function addPersonalizationHints( personId: string, brief?: CommunicationBrief ): Promise { if (!brief) return brief; const userId = personToUserId(personId); try { const res = await request(`/v1/retrieve?user_id=${userId}&limit=5`); if (!res.ok) return brief; const data = await res.json(); if (!Array.isArray(data?.items) || !data.items.length) return brief; const items = data.items as RetrievedItem[]; const hintLines = items.map((item) => { const category = item.category ?? "general"; const title = item.title ?? `item-${item.item_id}`; return `tidal hint: ${category} style (score ${item.score.toFixed(2)}) via \"${title}\"`; }); return { ...brief, observations: [...hintLines, ...brief.observations].slice(0, 8), }; } catch (err) { const msg = err instanceof Error ? err.message : String(err); console.error(`[tidal] personalization hints failed: ${msg}`); return brief; } }