Source code for veupath_chatbot.ai.tools.conversation_tools
"""Tools for conversation and strategy management."""
from typing import Annotated
from uuid import UUID
from kani import AIParam, ai_function
from veupath_chatbot.domain.strategy.session import StrategySession
from veupath_chatbot.platform.errors import ErrorCode
from veupath_chatbot.platform.logging import get_logger
from veupath_chatbot.platform.tool_errors import tool_error
from veupath_chatbot.platform.types import JSONObject
logger = get_logger(__name__)
[docs]
class ConversationTools:
"""Tools for managing conversations and saved strategies."""
[docs]
def __init__(
self,
session: StrategySession,
user_id: UUID | None = None,
) -> None:
self.session = session
self.user_id = user_id
[docs]
@ai_function()
async def save_strategy(
self,
name: Annotated[str, AIParam(desc="Name for the saved strategy")],
description: Annotated[str | None, AIParam(desc="Description")] = None,
graph_id: Annotated[str | None, AIParam(desc="Graph ID to save")] = None,
) -> JSONObject:
"""Save the current strategy for later use.
The strategy will be saved to the user's account and can be
loaded again in future conversations.
"""
graph = self.session.get_graph(graph_id)
if not graph:
return tool_error(ErrorCode.NOT_FOUND, "Graph not found", graphId=graph_id)
if not graph.current_strategy:
return tool_error(
ErrorCode.INVALID_STRATEGY,
"No strategy to save. Build a strategy first.",
)
strategy = graph.current_strategy
strategy.name = name
if description is not None:
strategy.description = description
graph.name = name
# In production, this would save to database
logger.info("Saving strategy", name=name, user_id=str(self.user_id))
return {
"ok": True,
"graphId": graph.id,
"name": name,
"description": strategy.description,
"recordType": strategy.record_type,
"graphName": graph.name,
"plan": strategy.to_dict(),
"message": f"Strategy '{name}' saved successfully.",
}
[docs]
@ai_function()
async def rename_strategy(
self,
new_name: Annotated[str, AIParam(desc="New name for the strategy")],
description: Annotated[str, AIParam(desc="Strategy description")],
graph_id: Annotated[str | None, AIParam(desc="Graph ID to rename")] = None,
) -> JSONObject:
"""Rename the current strategy."""
graph = self.session.get_graph(graph_id)
if not graph:
return tool_error(ErrorCode.NOT_FOUND, "Graph not found", graphId=graph_id)
if not graph.current_strategy:
return tool_error(ErrorCode.INVALID_STRATEGY, "No strategy to rename.")
old_name = graph.current_strategy.name
graph.current_strategy.name = new_name
graph.current_strategy.description = description
graph.name = new_name
graph.save_history(f"Renamed from '{old_name}' to '{new_name}'")
return {
"ok": True,
"graphId": graph.id,
"oldName": old_name,
"newName": new_name,
"name": new_name,
"recordType": graph.current_strategy.record_type,
"description": graph.current_strategy.description,
"plan": graph.current_strategy.to_dict(),
}
[docs]
@ai_function()
async def clear_strategy(
self,
graph_id: Annotated[str | None, AIParam(desc="Graph ID to clear")] = None,
confirm: Annotated[
bool,
AIParam(desc="Set true to confirm deleting all nodes in the graph"),
] = False,
) -> JSONObject:
"""Clear the current strategy and start fresh.
This removes all steps and the current strategy. Requires explicit confirmation.
"""
graph = self.session.get_graph(graph_id)
if not graph:
return tool_error(ErrorCode.NOT_FOUND, "Graph not found", graphId=graph_id)
if not confirm:
return tool_error(
ErrorCode.VALIDATION_ERROR,
"Refusing to clear the strategy without confirmation. Use confirm=true.",
graphId=graph.id,
requiresConfirmation=True,
)
graph.steps.clear()
graph.roots.clear()
graph.current_strategy = None
graph.history.clear()
graph.last_step_id = None
graph.wdk_strategy_id = None
graph.wdk_step_ids.clear()
graph.step_counts.clear()
return {
"ok": True,
"graphId": graph.id,
"cleared": True,
"message": "Strategy cleared. Ready to start fresh.",
}
[docs]
@ai_function()
async def get_strategy_summary(
self,
graph_id: Annotated[str | None, AIParam(desc="Graph ID to summarize")] = None,
) -> JSONObject:
"""Get a summary of the current strategy.
Returns step count, record type, and other metadata.
"""
graph = self.session.get_graph(graph_id)
if not graph:
return tool_error(ErrorCode.NOT_FOUND, "Graph not found", graphId=graph_id)
if not graph.current_strategy:
return {
"hasStrategy": False,
"graphId": graph.id,
"graphName": graph.name,
"stepCount": len(graph.steps),
"message": (
f"No complete strategy yet. {len(graph.steps)} steps created."
),
}
strategy = graph.current_strategy
return {
"hasStrategy": True,
"graphId": graph.id,
"graphName": graph.name,
"name": strategy.name,
"recordType": strategy.record_type,
"stepCount": len(strategy.get_all_steps()),
"description": strategy.description,
}