192 lines
7.9 KiB
Python
192 lines
7.9 KiB
Python
"""Parlant client utilities for demo communication."""
|
|
import parlant.sdk as p
|
|
from parlant.client import AsyncParlantClient
|
|
from typing import Optional
|
|
import asyncio
|
|
import os
|
|
|
|
|
|
async def create_client(base_url: str = "") -> AsyncParlantClient:
|
|
"""Create a Parlant client connection."""
|
|
resolved_base_url = base_url or os.getenv("PARLANT_BASE_URL", "http://127.0.0.1:8800")
|
|
return AsyncParlantClient(base_url=resolved_base_url)
|
|
|
|
|
|
async def create_session(client: AsyncParlantClient, agent_id: str, retries: int = 20, delay: float = 0.6) -> str:
|
|
"""Create a new Parlant session with retry logic."""
|
|
last_exc: Exception | None = None
|
|
for attempt in range(retries):
|
|
try:
|
|
session = await client.sessions.create(agent_id=agent_id)
|
|
return session.id if hasattr(session, "id") else session["id"]
|
|
except Exception as exc:
|
|
last_exc = exc
|
|
await asyncio.sleep(delay)
|
|
delay = min(3.0, delay * 1.5)
|
|
base_url = getattr(client, "_base_url", None) or os.getenv("PARLANT_BASE_URL", "http://127.0.0.1:8800")
|
|
raise last_exc or RuntimeError(f"Failed to create session after {retries} attempts (server at {base_url}?).")
|
|
|
|
|
|
async def send_user_message(client: AsyncParlantClient, session_id: str, message: str) -> int:
|
|
"""Send a user message to the Parlant session."""
|
|
event = await client.sessions.create_event(
|
|
session_id=session_id,
|
|
kind="message",
|
|
source="customer",
|
|
message=message,
|
|
)
|
|
return event.offset
|
|
|
|
|
|
async def await_ai_reply(client: AsyncParlantClient, session_id: str, min_offset: int) -> Optional[str]:
|
|
"""Wait for and collect all AI agent messages from a Parlant session."""
|
|
all_messages = []
|
|
current_offset = min_offset
|
|
max_polls = 3
|
|
|
|
for poll_count in range(max_polls):
|
|
try:
|
|
events = await client.sessions.list_events(
|
|
session_id=session_id,
|
|
kinds="message",
|
|
min_offset=current_offset,
|
|
wait_for_data=45,
|
|
)
|
|
except Exception as e:
|
|
if "timeout" in str(e).lower() or "504" in str(e):
|
|
break
|
|
raise
|
|
|
|
new_messages = []
|
|
max_offset = current_offset - 1
|
|
|
|
for event in events:
|
|
if event.source == "ai_agent" and event.kind == "message":
|
|
if isinstance(event.data, dict) and event.data.get("message"):
|
|
new_messages.append(event.data.get("message"))
|
|
max_offset = max(max_offset, event.offset)
|
|
|
|
if new_messages:
|
|
all_messages.extend(new_messages)
|
|
current_offset = max_offset + 1
|
|
else:
|
|
break
|
|
|
|
return "\n\n".join(all_messages) if all_messages else None
|
|
|
|
|
|
async def get_session_reasoning(client: AsyncParlantClient, session_id: str, min_offset: int = 0) -> str:
|
|
"""Summarize which guidelines and tools the agent used for this session."""
|
|
guidelines: list[str] = []
|
|
tools_used: list[str] = []
|
|
guideline_details: list[str] = []
|
|
|
|
# Get session state for applied guidelines
|
|
try:
|
|
session_info = await client.sessions.retrieve(session_id=session_id)
|
|
agent_states = getattr(session_info, "agent_states", None) or []
|
|
for state in agent_states:
|
|
ids = getattr(state, "applied_guideline_ids", None) or []
|
|
for gid in ids:
|
|
if gid not in guidelines:
|
|
guidelines.append(gid)
|
|
except Exception:
|
|
pass
|
|
|
|
# Scan events for tool calls and guideline details
|
|
try:
|
|
events = await client.sessions.list_events(
|
|
session_id=session_id,
|
|
min_offset=0,
|
|
wait_for_data=0,
|
|
)
|
|
|
|
for ev in events:
|
|
# Extract guidelines from status events
|
|
if ev.kind == "status" and isinstance(ev.data, dict):
|
|
applied = ev.data.get("applied_guidelines")
|
|
if isinstance(applied, list):
|
|
for g in applied:
|
|
if isinstance(g, dict):
|
|
name = g.get("name")
|
|
condition = g.get("condition", "")
|
|
action = g.get("action", "")
|
|
if name and name not in guidelines:
|
|
guidelines.append(name)
|
|
if condition and action:
|
|
guideline_details.append(f"{name}: {condition} -> {action[:50]}...")
|
|
|
|
guideline_matches = ev.data.get("guideline_matches")
|
|
if isinstance(guideline_matches, list):
|
|
for match in guideline_matches:
|
|
if isinstance(match, dict):
|
|
gid = match.get("guideline_id")
|
|
condition = match.get("condition", "")
|
|
action = match.get("action", "")
|
|
if gid and gid not in guidelines:
|
|
guidelines.append(gid)
|
|
if condition and action:
|
|
guideline_details.append(f"{gid}: {condition} -> {action[:50]}...")
|
|
|
|
# Extract tools from tool events
|
|
if ev.kind == "tool" and isinstance(ev.data, dict):
|
|
tool_calls = ev.data.get("tool_calls")
|
|
if isinstance(tool_calls, list):
|
|
for tool_call in tool_calls:
|
|
if isinstance(tool_call, dict):
|
|
name = tool_call.get("tool_id") or tool_call.get("name") or tool_call.get("function", {}).get("name")
|
|
if name and name not in tools_used:
|
|
tools_used.append(name)
|
|
|
|
if ev.kind in ("tool_call", "tool"):
|
|
data = ev.data if isinstance(ev.data, dict) else {}
|
|
name = (
|
|
data.get("name")
|
|
or data.get("tool_name")
|
|
or ((data.get("tool") or {}).get("name") if isinstance(data.get("tool"), dict) else None)
|
|
or ((data.get("result") or {}).get("tool_name") if isinstance(data.get("result"), dict) else None)
|
|
)
|
|
if name and name not in tools_used:
|
|
tools_used.append(name)
|
|
|
|
if ev.kind == "message" and isinstance(ev.data, dict):
|
|
meta = ev.data.get("meta") or {}
|
|
tool = meta.get("tool_name")
|
|
if tool and tool not in tools_used:
|
|
tools_used.append(tool)
|
|
except Exception:
|
|
pass
|
|
|
|
parts: list[str] = []
|
|
|
|
# Add guidelines if available
|
|
if guideline_details:
|
|
parts.append(f"Guidelines: {'; '.join(guideline_details)}")
|
|
elif guidelines:
|
|
parts.append(f"Guidelines: {', '.join(guidelines)}")
|
|
|
|
# Infer guidelines from tool usage (following Parlant's design)
|
|
if tools_used and not guideline_details and not guidelines:
|
|
if any("agent_contact" in tool for tool in tools_used):
|
|
parts.append("Guidelines: Policy replacement guideline applied")
|
|
elif any("coverage" in tool for tool in tools_used):
|
|
parts.append("Guidelines: Coverage calculation guideline applied")
|
|
elif any("health" in tool for tool in tools_used):
|
|
parts.append("Guidelines: Health impact assessment guideline applied")
|
|
elif any("application" in tool for tool in tools_used):
|
|
parts.append("Guidelines: Application process guideline applied")
|
|
else:
|
|
parts.append("Guidelines: Structured response guideline applied")
|
|
|
|
if tools_used:
|
|
parts.append(f"Tools: {', '.join(tools_used)}")
|
|
|
|
if not guideline_details and not guidelines and not tools_used:
|
|
parts.append("(no tools/guidelines recorded in session data)")
|
|
|
|
return " | ".join(parts) if parts else "(no explicit tools/guidelines recorded)"
|
|
|
|
|
|
|
|
|