Skip to content

Commit cdc23db

Browse files
committed
Explainability v1
1 parent 1b955c3 commit cdc23db

7 files changed

Lines changed: 615 additions & 62 deletions

File tree

anton/chat.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
handle_setup,
3636
handle_setup_models,
3737
)
38-
from anton.commands.ui import handle_theme, print_slash_help
38+
from anton.commands.ui import handle_explain, handle_theme, print_slash_help
3939
from anton.utils.clipboard import (
4040
ensure_clipboard,
4141
format_clipboard_image_message,
@@ -1273,6 +1273,9 @@ def _bottom_toolbar():
12731273
elif cmd == "/unpublish":
12741274
await _handle_unpublish(console, settings, workspace)
12751275
continue
1276+
elif cmd == "/explain":
1277+
handle_explain(console, settings.workspace_path)
1278+
continue
12761279
elif cmd == "/help":
12771280
print_slash_help(console)
12781281
continue

anton/commands/ui.py

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
"""Slash-command handlers for /theme and /help."""
1+
"""Slash-command handlers for /theme, /explain, and /help."""
22

33
from __future__ import annotations
44

55
from rich.console import Console
66

7+
from anton.explainability import ExplainabilityStore
8+
79

810
def handle_theme(console: Console, arg: str) -> None:
911
"""Switch the color theme (light/dark)."""
@@ -17,7 +19,9 @@ def handle_theme(console: Console, arg: str) -> None:
1719
elif arg in ("light", "dark"):
1820
new_mode = arg
1921
else:
20-
console.print(f"[anton.warning]Unknown theme '{arg}'. Use: /theme light | /theme dark[/]")
22+
console.print(
23+
f"[anton.warning]Unknown theme '{arg}'. Use: /theme light | /theme dark[/]"
24+
)
2125
console.print()
2226
return
2327

@@ -37,7 +41,9 @@ def print_slash_help(console: Console) -> None:
3741
console.print(" [bold]/llm[/] — Change LLM provider or API key")
3842

3943
console.print("\n[bold]Data Connections[/]")
40-
console.print(" [bold]/connect[/] — Connect a database or API to your Local Vault")
44+
console.print(
45+
" [bold]/connect[/] — Connect a database or API to your Local Vault"
46+
)
4147
console.print(" [bold]/list[/] — List all saved connections")
4248
console.print(" [bold]/edit[/] — Edit credentials for an existing connection")
4349
console.print(" [bold]/remove[/] — Remove a saved connection")
@@ -53,9 +59,62 @@ def print_slash_help(console: Console) -> None:
5359
console.print(" [bold]/resume[/] — Continue a previous session")
5460
console.print(" [bold]/publish[/] — Publish an HTML report to the web")
5561
console.print(" [bold]/unpublish[/] — Remove a published report")
62+
console.print(
63+
" [bold]/explain[/] — Show explainability details for the latest answer"
64+
)
5665

5766
console.print("\n[bold]General[/]")
5867
console.print(" [bold]/help[/] — Show this help menu")
5968
console.print(" [bold]exit[/] — Exit the chat")
6069

6170
console.print()
71+
72+
73+
def handle_explain(console: Console, workspace_path) -> None:
74+
"""Print explainability details for the latest answer in the workspace."""
75+
store = ExplainabilityStore(workspace_path)
76+
record = store.load_latest()
77+
if record is None:
78+
console.print(
79+
"[anton.warning]No explainability record found yet for this workspace.[/]"
80+
)
81+
console.print()
82+
return
83+
84+
console.print()
85+
console.print("[anton.cyan]Explain This Answer[/]")
86+
console.print(f"[anton.muted]Turn {record.turn}{record.created_at}[/]")
87+
console.print()
88+
89+
console.print("[bold]Summary[/]")
90+
console.print(record.summary or "No summary available.")
91+
console.print()
92+
93+
console.print("[bold]Data Sources Used[/]")
94+
if record.data_sources:
95+
for source in record.data_sources:
96+
engine = source.get("engine")
97+
if engine:
98+
console.print(f" - {source.get('name', 'Unknown')} ({engine})")
99+
else:
100+
console.print(f" - {source.get('name', 'Unknown')}")
101+
else:
102+
console.print(" - None captured")
103+
console.print()
104+
105+
console.print("[bold]Generated SQL[/]")
106+
if record.sql_queries:
107+
for i, query in enumerate(record.sql_queries, 1):
108+
header = f" Query {i}: {query.get('datasource', 'Unknown datasource')}"
109+
if query.get("engine"):
110+
header += f" ({query['engine']})"
111+
console.print(header)
112+
console.print("```sql")
113+
console.print(query.get("sql", ""))
114+
console.print("```")
115+
if query.get("status") == "error" and query.get("error_message"):
116+
console.print(f"[anton.warning]{query['error_message']}[/]")
117+
console.print()
118+
else:
119+
console.print(" - No SQL generated")
120+
console.print()

anton/core/session.py

Lines changed: 113 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
from anton.core.tools.tool_defs import SCRATCHPAD_TOOL, MEMORIZE_TOOL, RECALL_TOOL, ToolDef
2121
from anton.core.utils.scratchpad import prepare_scratchpad_exec, format_cell_result
2222

23+
from anton.explainability import ExplainabilityCollector, ExplainabilityStore
24+
from anton.llm.openai import build_chat_completion_kwargs
25+
2326
from anton.utils.datasources import (
2427
build_datasource_context,
2528
scrub_credentials,
@@ -136,6 +139,10 @@ def __init__(
136139
workspace_path=workspace.base if workspace else None,
137140
)
138141
self.tool_registry = ToolRegistry()
142+
self._explainability_store = (
143+
ExplainabilityStore(workspace.base) if workspace is not None else None
144+
)
145+
self._active_explainability: ExplainabilityCollector | None = None
139146

140147
@property
141148
def history(self) -> list[dict]:
@@ -184,6 +191,44 @@ def _persist_history(self) -> None:
184191
if self._history_store and self._session_id:
185192
self._history_store.save(self._session_id, self._history)
186193

194+
def _record_cell_explainability(
195+
self, *, pad_name: str, description: str, cell
196+
) -> None:
197+
if self._active_explainability is None:
198+
return
199+
if description:
200+
self._active_explainability.add_scratchpad_step(description)
201+
elif pad_name:
202+
self._active_explainability.add_scratchpad_step(
203+
f"work in scratchpad {pad_name}"
204+
)
205+
for query in getattr(cell, "explainability_queries", []) or []:
206+
if not isinstance(query, dict):
207+
continue
208+
self._active_explainability.add_query(
209+
datasource=str(query.get("datasource", "")),
210+
sql=str(query.get("sql", "")),
211+
engine=(
212+
str(query.get("engine"))
213+
if query.get("engine") is not None
214+
else None
215+
),
216+
status=str(query.get("status", "ok")),
217+
error_message=(
218+
str(query.get("error_message"))
219+
if query.get("error_message") is not None
220+
else None
221+
),
222+
)
223+
self._active_explainability.add_sources_from_text(
224+
getattr(cell, "code", ""),
225+
getattr(cell, "stdout", ""),
226+
getattr(cell, "logs", ""),
227+
)
228+
self._active_explainability.add_inferred_queries_from_code(
229+
getattr(cell, "code", "")
230+
)
231+
187232
async def _build_system_prompt(self, user_message: str = "") -> str:
188233
import datetime as _dt
189234
_now = _dt.datetime.now()
@@ -566,64 +611,75 @@ async def turn_stream(
566611
assistant_text_parts: list[str] = []
567612
_max_auto_retries = 2
568613
_retry_count = 0
614+
self._active_explainability = ExplainabilityCollector(
615+
self._explainability_store,
616+
turn=self._turn_count + 1,
617+
user_message=user_msg_str,
618+
)
569619

570-
while True:
571-
try:
572-
async for event in self._stream_and_handle_tools(user_msg_str):
573-
if isinstance(event, StreamTextDelta):
574-
assistant_text_parts.append(event.text)
575-
yield event
576-
break # completed successfully
577-
except Exception as _agent_exc:
578-
# Token/billing limit — don't retry, let the chat loop handle it
579-
if isinstance(_agent_exc, TokenLimitExceeded):
580-
raise
581-
_retry_count += 1
582-
if _retry_count <= _max_auto_retries:
583-
# Inject the error into history and let the LLM try to recover
584-
self._history.append(
585-
{
586-
"role": "user",
587-
"content": (
588-
f"SYSTEM: An error interrupted execution: {_agent_exc}\n\n"
589-
"If you can diagnose and fix the issue, continue working on the task. "
590-
"Adjust your approach to avoid the same error. "
591-
"If this is unrecoverable, summarize what you accomplished and suggest next steps."
592-
),
593-
}
594-
)
595-
# Continue the while loop — _stream_and_handle_tools will be called
596-
# again with the error context now in history
597-
continue
598-
else:
599-
# Exhausted retries — stop and summarize for the user
600-
self._history.append(
601-
{
602-
"role": "user",
603-
"content": (
604-
f"SYSTEM: The task has failed {_retry_count} times. Latest error: {_agent_exc}\n\n"
605-
"Stop retrying. Please:\n"
606-
"1. Summarize what you accomplished so far.\n"
607-
"2. Explain what went wrong in plain language.\n"
608-
"3. Suggest next steps — what the user can try (e.g. rephrase, "
609-
"simplify the request, or ask you to continue from where you left off).\n"
610-
"Be concise and helpful."
611-
),
612-
}
613-
)
614-
try:
615-
async for event in self._llm.plan_stream(
616-
system=await self._build_system_prompt(user_msg_str),
617-
messages=self._history,
618-
):
619-
if isinstance(event, StreamTextDelta):
620-
assistant_text_parts.append(event.text)
621-
yield event
622-
except Exception:
623-
fallback = f"An unexpected error occurred: {_agent_exc}. Please try again or rephrase your request."
624-
assistant_text_parts.append(fallback)
625-
yield StreamTextDelta(text=fallback)
626-
break
620+
try:
621+
while True:
622+
try:
623+
async for event in self._stream_and_handle_tools(user_msg_str):
624+
if isinstance(event, StreamTextDelta):
625+
assistant_text_parts.append(event.text)
626+
yield event
627+
break # completed successfully
628+
except Exception as _agent_exc:
629+
# Token/billing limit — don't retry, let the chat loop handle it
630+
if isinstance(_agent_exc, TokenLimitExceeded):
631+
raise
632+
_retry_count += 1
633+
if _retry_count <= _max_auto_retries:
634+
# Inject the error into history and let the LLM try to recover
635+
self._history.append(
636+
{
637+
"role": "user",
638+
"content": (
639+
f"SYSTEM: An error interrupted execution: {_agent_exc}\n\n"
640+
"If you can diagnose and fix the issue, continue working on the task. "
641+
"Adjust your approach to avoid the same error. "
642+
"If this is unrecoverable, summarize what you accomplished and suggest next steps."
643+
),
644+
}
645+
)
646+
# Continue the while loop — _stream_and_handle_tools will be called
647+
# again with the error context now in history
648+
continue
649+
else:
650+
# Exhausted retries — stop and summarize for the user
651+
self._history.append(
652+
{
653+
"role": "user",
654+
"content": (
655+
f"SYSTEM: The task has failed {_retry_count} times. Latest error: {_agent_exc}\n\n"
656+
"Stop retrying. Please:\n"
657+
"1. Summarize what you accomplished so far.\n"
658+
"2. Explain what went wrong in plain language.\n"
659+
"3. Suggest next steps — what the user can try (e.g. rephrase, "
660+
"simplify the request, or ask you to continue from where you left off).\n"
661+
"Be concise and helpful."
662+
),
663+
}
664+
)
665+
try:
666+
async for event in self._llm.plan_stream(
667+
system=await self._build_system_prompt(user_msg_str),
668+
messages=self._history,
669+
):
670+
if isinstance(event, StreamTextDelta):
671+
assistant_text_parts.append(event.text)
672+
yield event
673+
except Exception:
674+
fallback = f"An unexpected error occurred: {_agent_exc}. Please try again or rephrase your request."
675+
assistant_text_parts.append(fallback)
676+
yield StreamTextDelta(text=fallback)
677+
break
678+
finally:
679+
if self._active_explainability is not None:
680+
self._active_explainability.finalize(
681+
"".join(assistant_text_parts)[:2000]
682+
)
627683

628684
# Log assistant response to episodic memory
629685
if self._episodic is not None and assistant_text_parts:

0 commit comments

Comments
 (0)