MicroFish/backend/app/services/interviews/zep_writer.py

69 lines
3.0 KiB
Python

from __future__ import annotations
from typing import Any, Optional
from app.models.interview import (
LikertResponse, QSortResponse, DelphiRatingResponse, ScenarioResponse, SubagentKind,
)
class InterviewZepWriter:
"""Writes interview episodes (per-agent responses, aggregates) to a Zep graph.
Expects ``memory_updater`` to expose ``add_text_episode(graph_id, text)`` — that
is the method the real ``ZepGraphMemoryUpdater`` provides for synchronous text
writes outside the agent-activity batch pipeline. A no-op shim with the same
method is acceptable for tests and stub mode.
"""
def __init__(self, memory_updater, graph_id: str):
self.updater = memory_updater
self.graph_id = graph_id
def _emit(self, text: str) -> None:
if hasattr(self.updater, "add_text_episode"):
self.updater.add_text_episode(self.graph_id, text)
else:
raise RuntimeError(
"memory_updater is missing add_text_episode(graph_id, text); "
"InterviewZepWriter requires the explicit text-episode API."
)
def _summarize_likert(self, r: LikertResponse) -> str:
mean_v = sum(r.responses.values()) / max(len(r.responses), 1)
top = sorted(r.responses.items(), key=lambda kv: -kv[1])[:3]
bot = sorted(r.responses.items(), key=lambda kv: kv[1])[:3]
return (f"mean={mean_v:.2f}; agrees with {[k for k,_ in top]}; "
f"disagrees with {[k for k,_ in bot]}")
def _summarize_qsort(self, r: QSortResponse) -> str:
plus = [k for k, v in r.placements.items() if v >= 2]
minus = [k for k, v in r.placements.items() if v <= -2]
return f"+strongly:{plus}; -strongly:{minus}"
def _summarize_scenario(self, r: ScenarioResponse) -> str:
parts = [f"{sid}: des={rt.desirability} plaus={rt.plausibility}"
for sid, rt in r.ratings.items()]
return "; ".join(parts)
def write_per_agent(
self, subagent: SubagentKind, response: Any, agent_name: str,
phase: Optional[str] = None,
) -> None:
if isinstance(response, LikertResponse):
phase = phase or response.phase.value
summary = self._summarize_likert(response)
elif isinstance(response, QSortResponse):
phase = phase or "T1"
summary = self._summarize_qsort(response)
elif isinstance(response, ScenarioResponse):
phase = phase or "T1"
summary = self._summarize_scenario(response)
elif isinstance(response, DelphiRatingResponse):
phase = phase or f"T1/R{response.round}"
summary = f"round={response.round}; {len(response.ratings)} themes rated"
else:
phase = phase or "T1"
summary = str(response)[:200]
text = f"Agent {agent_name} (interview/{subagent.value}/{phase}): {summary}"
self._emit(text)
def write_aggregate(self, subagent: SubagentKind, summary: str) -> None:
self._emit(f"Interview aggregate ({subagent.value}): {summary}")