chore: linter fix
This commit is contained in:
@@ -251,9 +251,7 @@ class EmailReplyAgent:
|
||||
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}'"
|
||||
)
|
||||
errors.append(f"Entry point '{ep_id}' references unknown node '{nid}'")
|
||||
return {"valid": len(errors) == 0, "errors": errors, "warnings": warnings}
|
||||
|
||||
|
||||
|
||||
@@ -36,7 +36,9 @@ default_config = RuntimeConfig()
|
||||
class AgentMetadata:
|
||||
name: str = "Email Reply Agent"
|
||||
version: str = "1.0.0"
|
||||
description: str = "Filter unreplied emails, confirm recipients, send personalized replies."
|
||||
description: str = (
|
||||
"Filter unreplied emails, confirm recipients, send personalized replies."
|
||||
)
|
||||
intro_message: str = "Tell me which emails you want to reply to (e.g., 'emails from @company.com in the last week')."
|
||||
|
||||
|
||||
|
||||
@@ -6,9 +6,12 @@ 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"
|
||||
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)
|
||||
|
||||
|
||||
@@ -21,18 +24,28 @@ def cli():
|
||||
|
||||
@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(
|
||||
"--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))
|
||||
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)
|
||||
|
||||
|
||||
@@ -52,18 +65,37 @@ def tui():
|
||||
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)
|
||||
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())
|
||||
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())
|
||||
|
||||
|
||||
@@ -71,18 +103,24 @@ def tui():
|
||||
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'])}")
|
||||
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")
|
||||
if v["valid"]:
|
||||
click.echo("Agent is valid")
|
||||
else:
|
||||
click.echo("Errors:")
|
||||
for e in v["errors"]: click.echo(f" {e}")
|
||||
for e in v["errors"]:
|
||||
click.echo(f" {e}")
|
||||
sys.exit(0 if v["valid"] else 1)
|
||||
|
||||
|
||||
|
||||
@@ -20,15 +20,54 @@ goal = 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),
|
||||
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"),
|
||||
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",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -37,14 +76,29 @@ 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),
|
||||
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),
|
||||
EdgeSpec(
|
||||
id="confirm-to-intake",
|
||||
source="confirm",
|
||||
target="intake",
|
||||
condition=EdgeCondition.CONDITIONAL,
|
||||
condition_expr="str(next_action).lower() == 'another'",
|
||||
priority=1,
|
||||
),
|
||||
]
|
||||
|
||||
# Graph configuration
|
||||
@@ -56,7 +110,11 @@ 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}
|
||||
loop_config = {
|
||||
"max_iterations": 100,
|
||||
"max_tool_calls_per_turn": 20,
|
||||
"max_history_tokens": 32000,
|
||||
}
|
||||
|
||||
|
||||
class MeetingScheduler:
|
||||
@@ -99,17 +157,36 @@ class MeetingScheduler:
|
||||
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)
|
||||
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),
|
||||
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):
|
||||
@@ -123,26 +200,37 @@ class MeetingScheduler:
|
||||
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):
|
||||
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)
|
||||
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)
|
||||
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,
|
||||
"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,
|
||||
"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],
|
||||
}
|
||||
@@ -151,13 +239,18 @@ class MeetingScheduler:
|
||||
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")
|
||||
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")
|
||||
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}'")
|
||||
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}
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,11 @@ intake_node = NodeSpec(
|
||||
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"],
|
||||
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.
|
||||
@@ -37,7 +41,13 @@ schedule_node = NodeSpec(
|
||||
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"],
|
||||
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="""\
|
||||
|
||||
@@ -18,6 +18,7 @@ AGENT_PATH = str(Path(__file__).resolve().parents[1])
|
||||
def agent_module():
|
||||
"""Import the agent package for structural validation."""
|
||||
import importlib
|
||||
|
||||
return importlib.import_module(Path(AGENT_PATH).name)
|
||||
|
||||
|
||||
@@ -26,6 +27,7 @@ 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:
|
||||
|
||||
@@ -2,8 +2,16 @@
|
||||
|
||||
import pytest
|
||||
from meeting_scheduler import (
|
||||
default_agent, goal, nodes, edges, entry_node, entry_points,
|
||||
terminal_nodes, conversation_mode, identity_prompt, loop_config
|
||||
default_agent,
|
||||
goal,
|
||||
nodes,
|
||||
edges,
|
||||
entry_node,
|
||||
entry_points,
|
||||
terminal_nodes,
|
||||
conversation_mode,
|
||||
identity_prompt,
|
||||
loop_config,
|
||||
)
|
||||
|
||||
|
||||
@@ -30,7 +38,12 @@ class TestNodeStructure:
|
||||
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"}
|
||||
required = {
|
||||
"calendar_check_availability",
|
||||
"calendar_create_event",
|
||||
"google_sheets_append_values",
|
||||
"send_email",
|
||||
}
|
||||
actual = set(nodes[1].tools)
|
||||
assert required.issubset(actual)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user