feat: meeting scheduler agent
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
"""Meeting Scheduler — Find available times on your calendar and book meetings."""
|
||||
|
||||
from .agent import (
|
||||
MeetingScheduler,
|
||||
default_agent,
|
||||
goal,
|
||||
nodes,
|
||||
edges,
|
||||
entry_node,
|
||||
entry_points,
|
||||
pause_nodes,
|
||||
terminal_nodes,
|
||||
conversation_mode,
|
||||
identity_prompt,
|
||||
loop_config,
|
||||
)
|
||||
from .config import default_config, metadata
|
||||
|
||||
__all__ = [
|
||||
"MeetingScheduler",
|
||||
"default_agent",
|
||||
"goal",
|
||||
"nodes",
|
||||
"edges",
|
||||
"entry_node",
|
||||
"entry_points",
|
||||
"pause_nodes",
|
||||
"terminal_nodes",
|
||||
"conversation_mode",
|
||||
"identity_prompt",
|
||||
"loop_config",
|
||||
"default_config",
|
||||
"metadata",
|
||||
]
|
||||
@@ -0,0 +1,90 @@
|
||||
"""CLI entry point for Meeting Scheduler."""
|
||||
|
||||
import asyncio, json, logging, sys
|
||||
import click
|
||||
from .agent import default_agent, MeetingScheduler
|
||||
|
||||
|
||||
def setup_logging(verbose=False, debug=False):
|
||||
if debug: level, fmt = logging.DEBUG, "%(asctime)s %(name)s: %(message)s"
|
||||
elif verbose: level, fmt = logging.INFO, "%(message)s"
|
||||
else: level, fmt = logging.WARNING, "%(levelname)s: %(message)s"
|
||||
logging.basicConfig(level=level, format=fmt, stream=sys.stderr)
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version="1.0.0")
|
||||
def cli():
|
||||
"""Meeting Scheduler — Find available times on your calendar and book meetings."""
|
||||
pass
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--attendee", "-a", required=True, help="Attendee email address")
|
||||
@click.option("--duration", "-d", type=int, required=True, help="Meeting duration in minutes")
|
||||
@click.option("--title", "-t", required=True, help="Meeting title")
|
||||
@click.option("--verbose", "-v", is_flag=True)
|
||||
def run(attendee, duration, title, verbose):
|
||||
"""Execute the scheduler."""
|
||||
setup_logging(verbose=verbose)
|
||||
result = asyncio.run(default_agent.run({
|
||||
"attendee_email": attendee,
|
||||
"meeting_duration_minutes": str(duration),
|
||||
"meeting_title": title,
|
||||
}))
|
||||
click.echo(json.dumps({"success": result.success, "output": result.output}, indent=2, default=str))
|
||||
sys.exit(0 if result.success else 1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
def tui():
|
||||
"""Launch TUI dashboard."""
|
||||
from pathlib import Path
|
||||
from framework.tui.app import AdenTUI
|
||||
from framework.llm import LiteLLMProvider
|
||||
from framework.runner.tool_registry import ToolRegistry
|
||||
from framework.runtime.agent_runtime import create_agent_runtime
|
||||
from framework.runtime.execution_stream import EntryPointSpec
|
||||
|
||||
async def run_tui():
|
||||
agent = MeetingScheduler()
|
||||
agent._tool_registry = ToolRegistry()
|
||||
storage = Path.home() / ".hive" / "agents" / "meeting_scheduler"
|
||||
storage.mkdir(parents=True, exist_ok=True)
|
||||
mcp_cfg = Path(__file__).parent / "mcp_servers.json"
|
||||
if mcp_cfg.exists(): agent._tool_registry.load_mcp_config(mcp_cfg)
|
||||
llm = LiteLLMProvider(model=agent.config.model, api_key=agent.config.api_key, api_base=agent.config.api_base)
|
||||
runtime = create_agent_runtime(
|
||||
graph=agent._build_graph(), goal=agent.goal, storage_path=storage,
|
||||
entry_points=[EntryPointSpec(id="start", name="Start", entry_node="intake", trigger_type="manual", isolation_level="isolated")],
|
||||
llm=llm, tools=list(agent._tool_registry.get_tools().values()), tool_executor=agent._tool_registry.get_executor())
|
||||
await runtime.start()
|
||||
try:
|
||||
app = AdenTUI(runtime)
|
||||
await app.run_async()
|
||||
finally:
|
||||
await runtime.stop()
|
||||
asyncio.run(run_tui())
|
||||
|
||||
|
||||
@cli.command()
|
||||
def info():
|
||||
"""Show agent info."""
|
||||
data = default_agent.info()
|
||||
click.echo(f"Agent: {data['name']}\nVersion: {data['version']}\nDescription: {data['description']}")
|
||||
click.echo(f"Nodes: {', '.join(data['nodes'])}\nClient-facing: {', '.join(data['client_facing_nodes'])}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
def validate():
|
||||
"""Validate agent structure."""
|
||||
v = default_agent.validate()
|
||||
if v["valid"]: click.echo("Agent is valid")
|
||||
else:
|
||||
click.echo("Errors:")
|
||||
for e in v["errors"]: click.echo(f" {e}")
|
||||
sys.exit(0 if v["valid"] else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
@@ -0,0 +1,164 @@
|
||||
"""Agent graph construction for Meeting Scheduler."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from framework.graph import EdgeSpec, EdgeCondition, Goal, SuccessCriterion, Constraint
|
||||
from framework.graph.edge import GraphSpec
|
||||
from framework.graph.executor import ExecutionResult
|
||||
from framework.graph.checkpoint_config import CheckpointConfig
|
||||
from framework.llm import LiteLLMProvider
|
||||
from framework.runner.tool_registry import ToolRegistry
|
||||
from framework.runtime.agent_runtime import AgentRuntime, create_agent_runtime
|
||||
from framework.runtime.execution_stream import EntryPointSpec
|
||||
|
||||
from .config import default_config, metadata
|
||||
from .nodes import intake_node, schedule_node, confirm_node
|
||||
|
||||
# Goal definition
|
||||
goal = Goal(
|
||||
id="meeting-scheduler-goal",
|
||||
name="Schedule Meetings",
|
||||
description="Check calendar availability, find optimal meeting times, record meetings, and send reminders.",
|
||||
success_criteria=[
|
||||
SuccessCriterion(id="sc-1", description="Meeting time found within requested duration", metric="calendar_availability", target="success", weight=0.35),
|
||||
SuccessCriterion(id="sc-2", description="Meeting recorded in spreadsheet accurately", metric="data_persistence", target="recorded", weight=0.30),
|
||||
SuccessCriterion(id="sc-3", description="Attendee email reminder sent", metric="communication", target="sent", weight=0.25),
|
||||
SuccessCriterion(id="sc-4", description="User confirms meeting details", metric="user_acknowledgment", target="confirmed", weight=0.10),
|
||||
],
|
||||
constraints=[
|
||||
Constraint(id="c-1", description="Must use Google Calendar API for availability check", constraint_type="hard", category="functional"),
|
||||
Constraint(id="c-2", description="Meeting duration must match requested time", constraint_type="hard", category="accuracy"),
|
||||
Constraint(id="c-3", description="Spreadsheet record must include date, time, attendee, title", constraint_type="hard", category="quality"),
|
||||
],
|
||||
)
|
||||
|
||||
# Node list
|
||||
nodes = [intake_node, schedule_node, confirm_node]
|
||||
|
||||
# Edge definitions
|
||||
edges = [
|
||||
EdgeSpec(id="intake-to-schedule", source="intake", target="schedule",
|
||||
condition=EdgeCondition.ON_SUCCESS, priority=1),
|
||||
EdgeSpec(id="schedule-to-confirm", source="schedule", target="confirm",
|
||||
condition=EdgeCondition.ON_SUCCESS, priority=1),
|
||||
# Loop back for another booking
|
||||
EdgeSpec(id="confirm-to-intake", source="confirm", target="intake",
|
||||
condition=EdgeCondition.CONDITIONAL,
|
||||
condition_expr="str(next_action).lower() == 'another'", priority=1),
|
||||
]
|
||||
|
||||
# Graph configuration
|
||||
entry_node = "intake"
|
||||
entry_points = {"start": "intake"}
|
||||
pause_nodes = []
|
||||
terminal_nodes = [] # Forever-alive
|
||||
|
||||
# Module-level vars read by AgentRunner.load()
|
||||
conversation_mode = "continuous"
|
||||
identity_prompt = "You are a helpful meeting scheduler assistant that manages calendar availability and sends confirmations."
|
||||
loop_config = {"max_iterations": 100, "max_tool_calls_per_turn": 20, "max_history_tokens": 32000}
|
||||
|
||||
|
||||
class MeetingScheduler:
|
||||
def __init__(self, config=None):
|
||||
self.config = config or default_config
|
||||
self.goal = goal
|
||||
self.nodes = nodes
|
||||
self.edges = edges
|
||||
self.entry_node = entry_node
|
||||
self.entry_points = entry_points
|
||||
self.pause_nodes = pause_nodes
|
||||
self.terminal_nodes = terminal_nodes
|
||||
self._graph = None
|
||||
self._agent_runtime = None
|
||||
self._tool_registry = None
|
||||
self._storage_path = None
|
||||
|
||||
def _build_graph(self):
|
||||
return GraphSpec(
|
||||
id="meeting-scheduler-graph",
|
||||
goal_id=self.goal.id,
|
||||
version="1.0.0",
|
||||
entry_node=self.entry_node,
|
||||
entry_points=self.entry_points,
|
||||
terminal_nodes=self.terminal_nodes,
|
||||
pause_nodes=self.pause_nodes,
|
||||
nodes=self.nodes,
|
||||
edges=self.edges,
|
||||
default_model=self.config.model,
|
||||
max_tokens=self.config.max_tokens,
|
||||
loop_config=loop_config,
|
||||
conversation_mode=conversation_mode,
|
||||
identity_prompt=identity_prompt,
|
||||
)
|
||||
|
||||
def _setup(self):
|
||||
self._storage_path = Path.home() / ".hive" / "agents" / "meeting_scheduler"
|
||||
self._storage_path.mkdir(parents=True, exist_ok=True)
|
||||
self._tool_registry = ToolRegistry()
|
||||
mcp_config = Path(__file__).parent / "mcp_servers.json"
|
||||
if mcp_config.exists():
|
||||
self._tool_registry.load_mcp_config(mcp_config)
|
||||
llm = LiteLLMProvider(model=self.config.model, api_key=self.config.api_key, api_base=self.config.api_base)
|
||||
tools = list(self._tool_registry.get_tools().values())
|
||||
tool_executor = self._tool_registry.get_executor()
|
||||
self._graph = self._build_graph()
|
||||
self._agent_runtime = create_agent_runtime(
|
||||
graph=self._graph, goal=self.goal, storage_path=self._storage_path,
|
||||
entry_points=[EntryPointSpec(id="default", name="Default", entry_node=self.entry_node,
|
||||
trigger_type="manual", isolation_level="shared")],
|
||||
llm=llm, tools=tools, tool_executor=tool_executor,
|
||||
checkpoint_config=CheckpointConfig(enabled=True, checkpoint_on_node_complete=True,
|
||||
checkpoint_max_age_days=7, async_checkpoint=True),
|
||||
)
|
||||
|
||||
async def start(self):
|
||||
if self._agent_runtime is None:
|
||||
self._setup()
|
||||
if not self._agent_runtime.is_running:
|
||||
await self._agent_runtime.start()
|
||||
|
||||
async def stop(self):
|
||||
if self._agent_runtime and self._agent_runtime.is_running:
|
||||
await self._agent_runtime.stop()
|
||||
self._agent_runtime = None
|
||||
|
||||
async def trigger_and_wait(self, entry_point="default", input_data=None, timeout=None, session_state=None):
|
||||
if self._agent_runtime is None:
|
||||
raise RuntimeError("Agent not started. Call start() first.")
|
||||
return await self._agent_runtime.trigger_and_wait(
|
||||
entry_point_id=entry_point, input_data=input_data or {}, session_state=session_state)
|
||||
|
||||
async def run(self, context, session_state=None):
|
||||
await self.start()
|
||||
try:
|
||||
result = await self.trigger_and_wait("default", context, session_state=session_state)
|
||||
return result or ExecutionResult(success=False, error="Execution timeout")
|
||||
finally:
|
||||
await self.stop()
|
||||
|
||||
def info(self):
|
||||
return {
|
||||
"name": metadata.name, "version": metadata.version, "description": metadata.description,
|
||||
"goal": {"name": self.goal.name, "description": self.goal.description},
|
||||
"nodes": [n.id for n in self.nodes], "edges": [e.id for e in self.edges],
|
||||
"entry_node": self.entry_node, "entry_points": self.entry_points,
|
||||
"terminal_nodes": self.terminal_nodes,
|
||||
"client_facing_nodes": [n.id for n in self.nodes if n.client_facing],
|
||||
}
|
||||
|
||||
def validate(self):
|
||||
errors, warnings = [], []
|
||||
node_ids = {n.id for n in self.nodes}
|
||||
for e in self.edges:
|
||||
if e.source not in node_ids: errors.append(f"Edge {e.id}: source '{e.source}' not found")
|
||||
if e.target not in node_ids: errors.append(f"Edge {e.id}: target '{e.target}' not found")
|
||||
if self.entry_node not in node_ids: errors.append(f"Entry node '{self.entry_node}' not found")
|
||||
for t in self.terminal_nodes:
|
||||
if t not in node_ids: errors.append(f"Terminal node '{t}' not found")
|
||||
for ep_id, nid in self.entry_points.items():
|
||||
if nid not in node_ids: errors.append(f"Entry point '{ep_id}' references unknown node '{nid}'")
|
||||
return {"valid": len(errors) == 0, "errors": errors, "warnings": warnings}
|
||||
|
||||
|
||||
default_agent = MeetingScheduler()
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Runtime configuration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from framework.config import RuntimeConfig
|
||||
|
||||
default_config = RuntimeConfig()
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentMetadata:
|
||||
name: str = "Meeting Scheduler"
|
||||
version: str = "1.0.0"
|
||||
description: str = (
|
||||
"Schedule meetings by checking Google Calendar availability, booking "
|
||||
"optimal time slots, recording details in Google Sheets, and sending "
|
||||
"email confirmations with Google Meet links to attendees."
|
||||
)
|
||||
intro_message: str = (
|
||||
"Hi! I'm your meeting scheduler. Tell me who you'd like to meet with, "
|
||||
"how long the meeting should be, and what it's about — I'll check "
|
||||
"calendar availability, book a time slot, log it to your spreadsheet, "
|
||||
"and send a confirmation email with a Google Meet link. "
|
||||
"Who would you like to schedule a meeting with?"
|
||||
)
|
||||
|
||||
|
||||
metadata = AgentMetadata()
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"hive-tools": {
|
||||
"transport": "stdio",
|
||||
"command": "uv",
|
||||
"args": ["run", "python", "mcp_server.py", "--stdio"],
|
||||
"cwd": "../../../tools",
|
||||
"description": "Hive tools MCP server"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Node definitions for Meeting Scheduler."""
|
||||
|
||||
from framework.graph import NodeSpec
|
||||
|
||||
# Node 1: Intake (client-facing)
|
||||
intake_node = NodeSpec(
|
||||
id="intake",
|
||||
name="Intake",
|
||||
description="Gather meeting details from the user",
|
||||
node_type="event_loop",
|
||||
client_facing=True,
|
||||
max_node_visits=0,
|
||||
input_keys=["attendee_email", "meeting_duration_minutes"],
|
||||
output_keys=["attendee_email", "meeting_duration_minutes", "meeting_title"],
|
||||
nullable_output_keys=["attendee_email", "meeting_duration_minutes", "meeting_title"],
|
||||
success_criteria="User has provided attendee email, meeting duration, and title.",
|
||||
system_prompt="""\
|
||||
You are a meeting scheduler assistant.
|
||||
|
||||
**STEP 1 — Use ask_user to collect meeting details:**
|
||||
1. Call ask_user to ask for: attendee email, meeting duration (minutes), and meeting title
|
||||
2. Wait for the user's response before proceeding
|
||||
|
||||
**STEP 2 — After user provides all details, call set_output:**
|
||||
- set_output("attendee_email", "user's email address")
|
||||
- set_output("meeting_duration_minutes", meeting duration as string)
|
||||
- set_output("meeting_title", "title of the meeting")
|
||||
""",
|
||||
tools=[],
|
||||
)
|
||||
|
||||
# Node 2: Schedule (autonomous)
|
||||
schedule_node = NodeSpec(
|
||||
id="schedule",
|
||||
name="Schedule",
|
||||
description="Find available time on calendar, book meeting with Google Meet, and log to Google Sheet",
|
||||
node_type="event_loop",
|
||||
max_node_visits=0,
|
||||
input_keys=["attendee_email", "meeting_duration_minutes", "meeting_title"],
|
||||
output_keys=["meeting_time", "booking_confirmed", "spreadsheet_recorded", "email_sent", "meet_link"],
|
||||
nullable_output_keys=[],
|
||||
success_criteria="Meeting time found, Google Meet created, Google Sheet 'Meeting Scheduler' updated with date/time/attendee/title/meet_link, and confirmation email sent.",
|
||||
system_prompt="""\
|
||||
You are a meeting booking agent that creates Google Calendar events with Google Meet and logs to Google Sheets.
|
||||
|
||||
## STEP 1 — Calendar Operations (tool calls in this turn):
|
||||
|
||||
1. **Find availability and verify conflicts:**
|
||||
- Use calendar_check_availability to find potential time slots.
|
||||
- **CRITICAL:** Always search a broad window (at least 8 hours) for the target day to see the full context of the user's schedule.
|
||||
- **SECONDARY CHECK:** Before finalizing a slot, use calendar_list_events for that specific day. This ensures you catch "soft" conflicts or events not marked as 'busy' that might still be important.
|
||||
|
||||
2. **Create the event WITH GOOGLE MEET (AUTOMATIC):**
|
||||
- Use calendar_create_event with these parameters:
|
||||
- summary: the meeting title
|
||||
- start_time: the start datetime in ISO format (e.g., "2024-01-15T09:00:00")
|
||||
- end_time: the end datetime in ISO format
|
||||
- attendees: list with the attendee email address (e.g., ["user@example.com"])
|
||||
- timezone: user's timezone (e.g., "America/Los_Angeles")
|
||||
- IMPORTANT: The tool automatically generates a Google Meet link when attendees are provided.
|
||||
You do NOT need to pass conferenceData - it is handled automatically.
|
||||
- The response will include conferenceData.entryPoints with the Google Meet link
|
||||
- Extract the meet_link from conferenceData.entryPoints[0].uri in the response
|
||||
|
||||
3. **Log to Google Sheets:**
|
||||
- First, use google_sheets_get_spreadsheet with spreadsheet_id="Meeting Scheduler" to check if it exists
|
||||
- If it doesn't exist, use google_sheets_create_spreadsheet with title="Meeting Scheduler"
|
||||
- Then use google_sheets_append_values to add a row with:
|
||||
- Date, Time, Attendee Email, Meeting Title, Google Meet Link
|
||||
- The spreadsheet_id should be "Meeting Scheduler" (by name) or the ID returned from create
|
||||
|
||||
4. **Send confirmation email:**
|
||||
- Use send_email to send the attendee a confirmation with:
|
||||
- to: attendee email address
|
||||
- subject: "Meeting Confirmation: {meeting_title}"
|
||||
- body: Include meeting title, date/time, and Google Meet link
|
||||
|
||||
## STEP 2 — set_output (SEPARATE turn, no other tool calls):
|
||||
|
||||
After all tools complete successfully, call set_output:
|
||||
- set_output("meeting_time", "YYYY-MM-DD HH:MM")
|
||||
- set_output("meet_link", "https://meet.google.com/xxx/yyy")
|
||||
- set_output("booking_confirmed", "true")
|
||||
- set_output("spreadsheet_recorded", "true")
|
||||
- set_output("email_sent", "true")
|
||||
|
||||
## CRITICAL: Google Meet creation
|
||||
Google Meet links are AUTOMATICALLY created by calendar_create_event when attendees are provided.
|
||||
Simply pass the attendees list and the tool will generate the Meet link.
|
||||
""",
|
||||
tools=[
|
||||
"calendar_check_availability",
|
||||
"calendar_create_event",
|
||||
"calendar_list_events",
|
||||
"google_sheets_create_spreadsheet",
|
||||
"google_sheets_get_spreadsheet",
|
||||
"google_sheets_append_values",
|
||||
"send_email",
|
||||
],
|
||||
)
|
||||
|
||||
# Node 3: Confirm (client-facing)
|
||||
confirm_node = NodeSpec(
|
||||
id="confirm",
|
||||
name="Confirm",
|
||||
description="Present booking confirmation to user with Google Meet link",
|
||||
node_type="event_loop",
|
||||
client_facing=True,
|
||||
max_node_visits=0,
|
||||
input_keys=["meeting_time", "booking_confirmed", "meet_link"],
|
||||
output_keys=["next_action"],
|
||||
nullable_output_keys=["next_action"],
|
||||
success_criteria="User has acknowledged the booking and received the Google Meet link.",
|
||||
system_prompt="""\
|
||||
You are a confirmation assistant.
|
||||
|
||||
**STEP 1 — Present confirmation and ask user:**
|
||||
1. Show the meeting details (date, time, attendee, title)
|
||||
2. Display the Google Meet link prominently
|
||||
3. Confirm the booking is complete and logged to Google Sheets
|
||||
4. Call ask_user to ask if they want to schedule another meeting or finish
|
||||
|
||||
**STEP 2 — After user responds, call set_output:**
|
||||
- set_output("next_action", "another") — if booking another meeting
|
||||
- set_output("next_action", "done") — if finished
|
||||
""",
|
||||
tools=[],
|
||||
)
|
||||
|
||||
__all__ = ["intake_node", "schedule_node", "confirm_node"]
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Test fixtures."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
_repo_root = Path(__file__).resolve().parents[4]
|
||||
for _p in ["examples/templates", "core"]:
|
||||
_path = str(_repo_root / _p)
|
||||
if _path not in sys.path:
|
||||
sys.path.insert(0, _path)
|
||||
|
||||
AGENT_PATH = str(Path(__file__).resolve().parents[1])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def agent_module():
|
||||
"""Import the agent package for structural validation."""
|
||||
import importlib
|
||||
return importlib.import_module(Path(AGENT_PATH).name)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def runner_loaded():
|
||||
"""Load the agent through AgentRunner (structural only, no LLM needed)."""
|
||||
from framework.runner.runner import AgentRunner
|
||||
from framework.credentials.models import CredentialError
|
||||
try:
|
||||
return AgentRunner.load(AGENT_PATH)
|
||||
except CredentialError:
|
||||
pytest.skip("Google OAuth credentials not configured")
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Structural tests for Meeting Scheduler."""
|
||||
|
||||
import pytest
|
||||
from meeting_scheduler import (
|
||||
default_agent, goal, nodes, edges, entry_node, entry_points,
|
||||
terminal_nodes, conversation_mode, identity_prompt, loop_config
|
||||
)
|
||||
|
||||
|
||||
class TestGoalDefinition:
|
||||
def test_goal_exists(self):
|
||||
assert goal is not None
|
||||
assert goal.id == "meeting-scheduler-goal"
|
||||
assert len(goal.success_criteria) == 4
|
||||
assert len(goal.constraints) == 3
|
||||
|
||||
def test_success_criteria_weights_sum_to_one(self):
|
||||
total = sum(sc.weight for sc in goal.success_criteria)
|
||||
assert abs(total - 1.0) < 0.01
|
||||
|
||||
|
||||
class TestNodeStructure:
|
||||
def test_three_nodes(self):
|
||||
assert len(nodes) == 3
|
||||
assert nodes[0].id == "intake"
|
||||
assert nodes[1].id == "schedule"
|
||||
assert nodes[2].id == "confirm"
|
||||
|
||||
def test_intake_is_client_facing(self):
|
||||
assert nodes[0].client_facing is True
|
||||
|
||||
def test_schedule_has_required_tools(self):
|
||||
required = {"calendar_check_availability", "calendar_create_event", "google_sheets_append_values", "send_email"}
|
||||
actual = set(nodes[1].tools)
|
||||
assert required.issubset(actual)
|
||||
|
||||
def test_confirm_is_client_facing(self):
|
||||
assert nodes[2].client_facing is True
|
||||
|
||||
|
||||
class TestEdgeStructure:
|
||||
def test_three_edges(self):
|
||||
assert len(edges) == 3
|
||||
|
||||
def test_linear_path(self):
|
||||
assert edges[0].source == "intake"
|
||||
assert edges[0].target == "schedule"
|
||||
assert edges[1].source == "schedule"
|
||||
assert edges[1].target == "confirm"
|
||||
|
||||
def test_loop_back(self):
|
||||
assert edges[2].source == "confirm"
|
||||
assert edges[2].target == "intake"
|
||||
|
||||
|
||||
class TestGraphConfiguration:
|
||||
def test_entry_node(self):
|
||||
assert entry_node == "intake"
|
||||
|
||||
def test_entry_points(self):
|
||||
assert entry_points == {"start": "intake"}
|
||||
|
||||
def test_forever_alive(self):
|
||||
assert terminal_nodes == []
|
||||
|
||||
def test_conversation_mode(self):
|
||||
assert conversation_mode == "continuous"
|
||||
|
||||
def test_loop_config_valid(self):
|
||||
assert "max_iterations" in loop_config
|
||||
assert "max_tool_calls_per_turn" in loop_config
|
||||
assert "max_history_tokens" in loop_config
|
||||
|
||||
|
||||
class TestAgentClass:
|
||||
def test_default_agent_created(self):
|
||||
assert default_agent is not None
|
||||
|
||||
def test_validate_passes(self):
|
||||
result = default_agent.validate()
|
||||
assert result["valid"] is True
|
||||
assert len(result["errors"]) == 0
|
||||
|
||||
def test_agent_info(self):
|
||||
info = default_agent.info()
|
||||
assert info["name"] == "Meeting Scheduler"
|
||||
assert "schedule" in [n for n in info["nodes"]]
|
||||
|
||||
|
||||
class TestRunnerLoad:
|
||||
def test_agent_runner_load_succeeds(self, runner_loaded):
|
||||
assert runner_loaded is not None
|
||||
Reference in New Issue
Block a user