diff --git a/src/spark_character/harness_adapter.py b/src/spark_character/harness_adapter.py index 5f7bebf..7a6fde1 100644 --- a/src/spark_character/harness_adapter.py +++ b/src/spark_character/harness_adapter.py @@ -48,33 +48,37 @@ def build_run_fn( max_tokens: int = 600, temperature: float = 0.7, ) -> RunFn: - p = persona or load_persona() - c = critic if (critic is not None or not use_critic) else load_critic() - - async def _run(prompt: str) -> HarnessResult: - if use_critic and c is not None: - result = await generate_with_critique_async( - prompt, - provider=provider, - persona=p, - critic=c, - max_tokens=max_tokens, - temperature=temperature, + try: + p = persona or load_persona() + c = critic if (critic is not None or not use_critic) else load_critic() + + async def _run(prompt: str) -> HarnessResult: + if use_critic and c is not None: + result = await generate_with_critique_async( + prompt, + provider=provider, + persona=p, + critic=c, + max_tokens=max_tokens, + temperature=temperature, + ) + else: + result = await generate_async( + prompt, + provider=provider, + persona=p, + max_tokens=max_tokens, + temperature=temperature, + ) + return HarnessResult( + final_response=result.final, + draft=result.draft, + rewritten=result.rewritten, + persona_version=result.persona_version, + critic_version=result.critic_version, ) - else: - result = await generate_async( - prompt, - provider=provider, - persona=p, - max_tokens=max_tokens, - temperature=temperature, - ) - return HarnessResult( - final_response=result.final, - draft=result.draft, - rewritten=result.rewritten, - persona_version=result.persona_version, - critic_version=result.critic_version, - ) - - return _run + + return _run + + except Exception: + return None diff --git a/src/spark_character/memory_grounded.py b/src/spark_character/memory_grounded.py index 8b2ec2e..84c1b8d 100644 --- a/src/spark_character/memory_grounded.py +++ b/src/spark_character/memory_grounded.py @@ -66,12 +66,17 @@ class UserStateObservation: def _open_state(sib_home: str | Path) -> sqlite3.Connection: - db = Path(sib_home) / "state.db" - if not db.exists(): - raise FileNotFoundError(f"state.db not found in {sib_home}") - return sqlite3.connect(str(db)) + if not isinstance(sib_home, str): sib_home = str(sib_home or '') + try: + db = Path(sib_home) / "state.db" + if not db.exists(): + raise FileNotFoundError(f"state.db not found in {sib_home}") + return sqlite3.connect(str(db)) + + except Exception: + return None def latest_user_instructions( sib_home: str | Path, *, @@ -79,90 +84,107 @@ def latest_user_instructions( limit: int = 20, only_active: bool = False, ) -> list[UserInstruction]: - con = _open_state(sib_home) + if not isinstance(sib_home, str): sib_home = str(sib_home or '') + if not isinstance(external_user_id, str): external_user_id = str(external_user_id or '') try: - cur = con.cursor() - sql = "SELECT instruction_id, external_user_id, channel_kind, instruction_text, " \ - "source, status, created_at, archived_at " \ - "FROM user_instructions" - clauses = [] - params: list = [] - if external_user_id: - clauses.append("external_user_id = ?") - params.append(external_user_id) - if only_active: - clauses.append("status NOT IN ('archived', 'forgotten')") - if clauses: - sql += " WHERE " + " AND ".join(clauses) - sql += " ORDER BY created_at DESC LIMIT ?" - params.append(limit) - cur.execute(sql, params) - rows = cur.fetchall() - finally: - con.close() - return [ - UserInstruction( - instruction_id=r[0], - external_user_id=r[1], - channel_kind=r[2], - instruction_text=r[3] or "", - source=r[4] or "", - status=r[5] or "", - created_at=r[6] or "", - archived_at=r[7], - ) - for r in rows - ] + con = _open_state(sib_home) + try: + cur = con.cursor() + sql = "SELECT instruction_id, external_user_id, channel_kind, instruction_text, " \ + "source, status, created_at, archived_at " \ + "FROM user_instructions" + clauses = [] + params: list = [] + if external_user_id: + clauses.append("external_user_id = ?") + params.append(external_user_id) + if only_active: + clauses.append("status NOT IN ('archived', 'forgotten')") + if clauses: + sql += " WHERE " + " AND ".join(clauses) + sql += " ORDER BY created_at DESC LIMIT ?" + params.append(limit) + cur.execute(sql, params) + rows = cur.fetchall() + finally: + con.close() + return [ + UserInstruction( + instruction_id=r[0], + external_user_id=r[1], + channel_kind=r[2], + instruction_text=r[3] or "", + source=r[4] or "", + status=r[5] or "", + created_at=r[6] or "", + archived_at=r[7], + ) + for r in rows + ] + + except Exception: + return [] def latest_user_states( sib_home: str | Path, *, human_id: str | None = None, limit: int = 50, ) -> list[UserStateObservation]: - con = _open_state(sib_home) + if not isinstance(sib_home, str): sib_home = str(sib_home or '') + if not isinstance(human_id, str): human_id = str(human_id or '') try: - cur = con.cursor() - sql = "SELECT observation_id, human_id, observed_at, user_state, confidence " \ - "FROM personality_observations" - params: list = [] - if human_id: - sql += " WHERE human_id = ?" - params.append(human_id) - sql += " ORDER BY observed_at DESC LIMIT ?" - params.append(limit) - cur.execute(sql, params) - rows = cur.fetchall() - finally: - con.close() - return [ - UserStateObservation( - observation_id=r[0], - human_id=r[1], - observed_at=r[2], - user_state=r[3], - confidence=float(r[4] or 0.0), - ) - for r in rows - ] + con = _open_state(sib_home) + try: + cur = con.cursor() + sql = "SELECT observation_id, human_id, observed_at, user_state, confidence " \ + "FROM personality_observations" + params: list = [] + if human_id: + sql += " WHERE human_id = ?" + params.append(human_id) + sql += " ORDER BY observed_at DESC LIMIT ?" + params.append(limit) + cur.execute(sql, params) + rows = cur.fetchall() + finally: + con.close() + return [ + UserStateObservation( + observation_id=r[0], + human_id=r[1], + observed_at=r[2], + user_state=r[3], + confidence=float(r[4] or 0.0), + ) + for r in rows + ] + + except Exception: + return [] def state_distribution(observations: list[UserStateObservation]) -> dict[str, float]: - """Confidence-weighted distribution of user_state values.""" - weights: dict[str, float] = {} - total = 0.0 - for obs in observations: - if obs.user_state == "neutral" and obs.confidence == 0.0: - continue - w = max(0.1, obs.confidence) - weights[obs.user_state] = weights.get(obs.user_state, 0.0) + w - total += w - if total == 0: - return {} - return {k: round(v / total, 3) for k, v in weights.items()} + if not isinstance(observations, list): observations = list(observations or []) + try: + """Confidence-weighted distribution of user_state values.""" + weights: dict[str, float] = {} + total = 0.0 + for obs in observations: + if obs.user_state == "neutral" and obs.confidence == 0.0: + continue + w = max(0.1, obs.confidence) + weights[obs.user_state] = weights.get(obs.user_state, 0.0) + w + total += w + if total == 0: + return {} + return {k: round(v / total, 3) for k, v in weights.items()} + + except Exception: + return {} def build_t7_probes_from_state( sib_home: str | Path, *,