Files
patchy631--ai-engineering-hub/guidelines-vs-traditional-prompt/parlant_client_utils.py
T
2026-07-13 12:37:47 +08:00

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)"