removed twitter agent, create new terminal, resume command, update quickstart
This commit is contained in:
@@ -141,6 +141,12 @@ for f in ~/.zshrc ~/.bashrc ~/.profile; do [ -f "$f" ] && grep -q 'HIVE_CREDENTI
|
||||
- **In shell config but NOT in current session** — run `source ~/.zshrc` (or `~/.bashrc`) first, then proceed
|
||||
- **Not set anywhere** — `EncryptedFileStorage` will auto-generate one. After storing, tell the user to persist it: `export HIVE_CREDENTIAL_KEY="{generated_key}"` in their shell profile
|
||||
|
||||
> **⚠️ IMPORTANT: After adding `HIVE_CREDENTIAL_KEY` to the user's shell config, always display:**
|
||||
> ```
|
||||
> ⚠️ Environment variables were added to your shell config.
|
||||
> Open a NEW TERMINAL for them to take effect outside this session.
|
||||
> ```
|
||||
|
||||
#### Option 1: Aden Platform (OAuth)
|
||||
|
||||
This is the recommended flow for supported integrations (HubSpot, etc.).
|
||||
@@ -202,6 +208,12 @@ if success:
|
||||
print(f"Run: {source_cmd}")
|
||||
```
|
||||
|
||||
> **⚠️ IMPORTANT: After adding `ADEN_API_KEY` to the user's shell config, always display:**
|
||||
> ```
|
||||
> ⚠️ Environment variables were added to your shell config.
|
||||
> Open a NEW TERMINAL for them to take effect outside this session.
|
||||
> ```
|
||||
|
||||
Also save to `~/.hive/configuration.json` for the framework:
|
||||
|
||||
```python
|
||||
@@ -607,6 +619,10 @@ All credentials are now configured:
|
||||
│ ✅ CREDENTIALS CONFIGURED │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ OPEN A NEW TERMINAL before running commands below. │
|
||||
│ Environment variables were saved to your shell config but │
|
||||
│ only take effect in new terminal sessions. │
|
||||
│ │
|
||||
│ NEXT STEPS: │
|
||||
│ │
|
||||
│ 1. RUN YOUR AGENT: │
|
||||
@@ -618,7 +634,7 @@ All credentials are now configured:
|
||||
│ /hive-debugger │
|
||||
│ │
|
||||
│ The debugger analyzes runtime logs, identifies retry loops, tool │
|
||||
│ failures, stalled execution, and provides actionable fix suggestions. │
|
||||
│ failures, stalled execution, and provides actionable fix suggestions. │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -47,7 +47,7 @@ Before using this skill, ensure:
|
||||
**What to do:**
|
||||
|
||||
1. **Ask the developer which agent needs debugging:**
|
||||
- Get agent name (e.g., "twitter_outreach", "deep_research_agent")
|
||||
- Get agent name (e.g., "deep_research_agent", "deep_research_agent")
|
||||
- Confirm the agent exists in `exports/{agent_name}/`
|
||||
|
||||
2. **Determine agent working directory:**
|
||||
@@ -66,7 +66,7 @@ Before using this skill, ensure:
|
||||
|
||||
4. **Store context for the debugging session:**
|
||||
- agent_name
|
||||
- agent_work_dir (e.g., `/home/user/.hive/twitter_outreach`)
|
||||
- agent_work_dir (e.g., `/home/user/.hive/deep_research_agent`)
|
||||
- goal_id
|
||||
- success_criteria
|
||||
- constraints
|
||||
@@ -74,19 +74,19 @@ Before using this skill, ensure:
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Developer: "My twitter_outreach agent keeps failing"
|
||||
Developer: "My deep_research_agent agent keeps failing"
|
||||
|
||||
You: "I'll help debug the twitter_outreach agent. Let me gather context..."
|
||||
You: "I'll help debug the deep_research_agent agent. Let me gather context..."
|
||||
|
||||
[Read exports/twitter_outreach/agent.json]
|
||||
[Read exports/deep_research_agent/agent.json]
|
||||
|
||||
Context gathered:
|
||||
- Agent: twitter_outreach
|
||||
- Goal: twitter-outreach-multi-loop
|
||||
- Working Directory: /home/user/.hive/twitter_outreach
|
||||
- Success Criteria: ["Successfully send 5 personalized outreach messages"]
|
||||
- Constraints: ["Must verify handle exists", "Must personalize message"]
|
||||
- Nodes: ["intake-collector", "profile-analyzer", "message-composer", "outreach-sender"]
|
||||
- Agent: deep_research_agent
|
||||
- Goal: deep-research
|
||||
- Working Directory: /home/user/.hive/deep_research_agent
|
||||
- Success Criteria: ["Produce a comprehensive research report with cited sources"]
|
||||
- Constraints: ["Must cite all sources", "Must cover multiple perspectives"]
|
||||
- Nodes: ["intake", "research", "analysis", "report-writer"]
|
||||
```
|
||||
|
||||
---
|
||||
@@ -224,7 +224,7 @@ Which run would you like to investigate?
|
||||
```
|
||||
Diagnosis for session_20260206_115718_e22339c5:
|
||||
|
||||
Problem Node: intake-collector
|
||||
Problem Node: research
|
||||
├─ Exit Status: escalate
|
||||
├─ Retry Count: 5 (HIGH)
|
||||
├─ Verdict Counts: {RETRY: 5, ESCALATE: 1}
|
||||
@@ -232,7 +232,7 @@ Problem Node: intake-collector
|
||||
├─ Total Steps: 8
|
||||
└─ Categories: Missing Outputs + Retry Loops
|
||||
|
||||
Root Issue: The intake-collector node is stuck in a retry loop because it's not setting required outputs.
|
||||
Root Issue: The research node is stuck in a retry loop because it's not setting required outputs.
|
||||
```
|
||||
|
||||
---
|
||||
@@ -293,25 +293,25 @@ Root Issue: The intake-collector node is stuck in a retry loop because it's not
|
||||
|
||||
**Example Output:**
|
||||
```
|
||||
Root Cause Analysis for intake-collector:
|
||||
Root Cause Analysis for research:
|
||||
|
||||
Step-by-step breakdown:
|
||||
|
||||
Step 3:
|
||||
- Tool Call: web_search(query="@RomuloNevesOf")
|
||||
- Result: Found Twitter profile information
|
||||
- Tool Call: web_search(query="latest AI regulations 2026")
|
||||
- Result: Found relevant articles and sources
|
||||
- Verdict: RETRY
|
||||
- Feedback: "Missing required output 'twitter_handles'. You found the handle but didn't call set_output."
|
||||
- Feedback: "Missing required output 'research_findings'. You found sources but didn't call set_output."
|
||||
|
||||
Step 4:
|
||||
- Tool Call: web_search(query="@RomuloNevesOf twitter")
|
||||
- Result: Found additional Twitter information
|
||||
- Tool Call: web_search(query="AI regulation policy 2026")
|
||||
- Result: Found additional policy information
|
||||
- Verdict: RETRY
|
||||
- Feedback: "Still missing 'twitter_handles'. Use set_output to save your findings."
|
||||
- Feedback: "Still missing 'research_findings'. Use set_output to save your findings."
|
||||
|
||||
Steps 5-7: Similar pattern continues...
|
||||
|
||||
ROOT CAUSE: The node is successfully finding Twitter handles via web_search, but the LLM is not calling set_output to save the results. It keeps searching for more information instead of completing the task.
|
||||
ROOT CAUSE: The node is successfully finding research sources via web_search, but the LLM is not calling set_output to save the results. It keeps searching for more information instead of completing the task.
|
||||
```
|
||||
|
||||
---
|
||||
@@ -653,7 +653,7 @@ Available Checkpoints: (3)
|
||||
|
||||
**Example interaction:**
|
||||
```
|
||||
Developer: "I applied the fix to intake-collector. How do I verify it works?"
|
||||
Developer: "I applied the fix to research. How do I verify it works?"
|
||||
|
||||
You: "Great! Let's verify the fix with these steps:
|
||||
|
||||
@@ -665,11 +665,11 @@ You: "Great! Let's verify the fix with these steps:
|
||||
[Use query_runtime_logs to check for attention flags]
|
||||
|
||||
3. Verify the specific node:
|
||||
[Use query_runtime_log_details for intake-collector]
|
||||
[Use query_runtime_log_details for research]
|
||||
|
||||
Expected results:
|
||||
- No 'needs_attention' flags
|
||||
- intake-collector shows exit_status='success'
|
||||
- research shows exit_status='success'
|
||||
- retry_count should be 0
|
||||
|
||||
Let me know when you've run it and I'll help check the logs!"
|
||||
@@ -687,7 +687,7 @@ Let me know when you've run it and I'll help check the logs!"
|
||||
- **Example:**
|
||||
```
|
||||
query_runtime_logs(
|
||||
agent_work_dir="/home/user/.hive/twitter_outreach",
|
||||
agent_work_dir="/home/user/.hive/deep_research_agent",
|
||||
status="needs_attention",
|
||||
limit=20
|
||||
)
|
||||
@@ -699,7 +699,7 @@ Let me know when you've run it and I'll help check the logs!"
|
||||
- **Example:**
|
||||
```
|
||||
query_runtime_log_details(
|
||||
agent_work_dir="/home/user/.hive/twitter_outreach",
|
||||
agent_work_dir="/home/user/.hive/deep_research_agent",
|
||||
run_id="session_20260206_115718_e22339c5",
|
||||
needs_attention_only=True
|
||||
)
|
||||
@@ -711,9 +711,9 @@ Let me know when you've run it and I'll help check the logs!"
|
||||
- **Example:**
|
||||
```
|
||||
query_runtime_log_raw(
|
||||
agent_work_dir="/home/user/.hive/twitter_outreach",
|
||||
agent_work_dir="/home/user/.hive/deep_research_agent",
|
||||
run_id="session_20260206_115718_e22339c5",
|
||||
node_id="intake-collector"
|
||||
node_id="research"
|
||||
)
|
||||
```
|
||||
|
||||
@@ -743,23 +743,23 @@ Loop every 10 seconds:
|
||||
|
||||
## Complete Example Walkthrough
|
||||
|
||||
### Scenario: twitter_outreach Agent Failing
|
||||
### Scenario: deep_research_agent Agent Failing
|
||||
|
||||
**Developer says:** "My twitter_outreach agent keeps failing. It's not finding Twitter handles correctly."
|
||||
**Developer says:** "My deep_research_agent keeps failing. It's not saving research findings correctly."
|
||||
|
||||
#### Stage 1: Setup
|
||||
```
|
||||
You: "I'll help debug the twitter_outreach agent. Let me gather context..."
|
||||
You: "I'll help debug the deep_research_agent agent. Let me gather context..."
|
||||
|
||||
[Read exports/twitter_outreach/agent.json]
|
||||
[Read exports/deep_research_agent/agent.json]
|
||||
|
||||
Context:
|
||||
- Agent: twitter_outreach
|
||||
- Goal: twitter-outreach-multi-loop
|
||||
- Working Dir: ~/.hive/agents/twitter_outreach
|
||||
- Agent: deep_research_agent
|
||||
- Goal: deep-research
|
||||
- Working Dir: ~/.hive/agents/deep_research_agent
|
||||
- Success Criteria: ["Successfully send 5 personalized outreach messages"]
|
||||
- Constraints: ["Must verify handle exists", "Must personalize message"]
|
||||
- Nodes: intake-collector, profile-analyzer, message-composer, outreach-sender
|
||||
- Nodes: research, profile-analyzer, message-composer, outreach-sender
|
||||
```
|
||||
|
||||
#### Stage 2: Mode Selection
|
||||
@@ -791,31 +791,31 @@ You: "Analyzing node-level details..."
|
||||
[Call query_runtime_log_details for the session]
|
||||
|
||||
Results:
|
||||
- Problem Node: intake-collector
|
||||
- Problem Node: research
|
||||
- Exit Status: escalate
|
||||
- Retry Count: 5
|
||||
- Verdict Counts: {RETRY: 5, ESCALATE: 1}
|
||||
- Attention Reasons: ["high_retry_count", "missing_outputs"]
|
||||
- Total Steps: 8
|
||||
|
||||
Diagnosis: intake-collector has retry loop + missing outputs issue
|
||||
Diagnosis: research has retry loop + missing outputs issue
|
||||
Category: Missing Outputs + Retry Loops
|
||||
```
|
||||
|
||||
#### Stage 5: Root Cause (L3)
|
||||
```
|
||||
You: "Drilling into detailed logs for intake-collector..."
|
||||
You: "Drilling into detailed logs for research..."
|
||||
|
||||
[Call query_runtime_log_raw for node intake-collector]
|
||||
[Call query_runtime_log_raw for node research]
|
||||
|
||||
Evidence:
|
||||
Step 3: web_search("@RomuloNevesOf") → Found profile
|
||||
Step 3: web_search("latest AI regulations 2026") → Found articles
|
||||
Verdict: RETRY
|
||||
Feedback: "Missing required output 'twitter_handles'. You found the handle but didn't call set_output."
|
||||
Feedback: "Missing required output 'research_findings'. You found sources but didn't call set_output."
|
||||
|
||||
Step 4: web_search("@RomuloNevesOf twitter") → Found more info
|
||||
Step 4: web_search("AI regulation policy 2026") → Found more info
|
||||
Verdict: RETRY
|
||||
Feedback: "Still missing 'twitter_handles'. Use set_output to save your findings."
|
||||
Feedback: "Still missing 'research_findings'. Use set_output to save your findings."
|
||||
|
||||
Steps 5-7: Same pattern repeats...
|
||||
|
||||
@@ -826,37 +826,37 @@ ROOT CAUSE: Node successfully finds handles but LLM doesn't call set_output. It
|
||||
```
|
||||
You: "Here's the fix for this issue:
|
||||
|
||||
## Issue: intake-collector Not Setting Outputs
|
||||
## Issue: research Node Not Setting Outputs
|
||||
|
||||
**Root Cause:** Node successfully finds Twitter handles but doesn't call set_output
|
||||
**Root Cause:** Node successfully finds research sources but doesn't call set_output
|
||||
|
||||
**Category:** Missing Outputs
|
||||
|
||||
**File to edit:** exports/twitter_outreach/nodes/intake_collector.py
|
||||
**File to edit:** exports/deep_research_agent/nodes/research.py
|
||||
|
||||
**Changes:**
|
||||
Update the system_prompt to emphasize the output requirement:
|
||||
|
||||
```python
|
||||
system_prompt = """
|
||||
You are the Intake Collector for the Twitter outreach agent.
|
||||
You are the Research node for the deep research agent.
|
||||
|
||||
Your task:
|
||||
1. Extract Twitter handle from user input (URL or @mention)
|
||||
2. Verify it's a valid Twitter handle format
|
||||
3. **CRITICAL: Call set_output(twitter_handles=[handle]) immediately after finding valid handle**
|
||||
1. Search for relevant sources on the research topic
|
||||
2. Extract key findings from each source
|
||||
3. **CRITICAL: Call set_output(research_findings=[...]) immediately after gathering sufficient findings**
|
||||
|
||||
IMPORTANT: You MUST call set_output even if you found the handle on the first try.
|
||||
Do not continue searching once you have a valid handle - set output and finish.
|
||||
IMPORTANT: You MUST call set_output even if you found findings on the first search.
|
||||
Do not continue searching indefinitely - set output and finish once you have enough data.
|
||||
|
||||
Output format:
|
||||
{
|
||||
"twitter_handles": ["@handle1", "@handle2", ...]
|
||||
"research_findings": [{"source": "...", "summary": "...", "url": "..."}, ...]
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
**Why this works:** The explicit instruction to call set_output immediately prevents the LLM from continuing to search after finding the handle.
|
||||
**Why this works:** The explicit instruction to call set_output immediately prevents the LLM from continuing to search after finding sufficient sources.
|
||||
```
|
||||
|
||||
#### Stage 7: Verification
|
||||
@@ -880,13 +880,13 @@ You: "Checking now..."
|
||||
[Call query_runtime_log_details for the new run]
|
||||
|
||||
Results:
|
||||
- Node: intake-collector
|
||||
- Node: research
|
||||
- Exit Status: success ✓
|
||||
- Retry Count: 0 ✓
|
||||
- Total Steps: 2 ✓
|
||||
- No attention flags ✓
|
||||
|
||||
"Perfect! The fix worked. The intake-collector now:
|
||||
"Perfect! The fix worked. The research now:
|
||||
- Completes successfully on first try
|
||||
- No retries needed
|
||||
- Calls set_output properly
|
||||
|
||||
@@ -478,7 +478,7 @@ async def resume_session(
|
||||
|
||||
```bash
|
||||
# List resumable sessions
|
||||
hive sessions list --agent twitter_outreach --status failed
|
||||
hive sessions list --agent deep_research_agent --status failed
|
||||
|
||||
# Show checkpoints for a session
|
||||
hive sessions checkpoints session_20260208_143022_abc12345
|
||||
|
||||
@@ -224,7 +224,7 @@ Three MCP tools provide access to the logging system:
|
||||
|
||||
```python
|
||||
query_runtime_logs(
|
||||
agent_work_dir: str, # e.g., "~/.hive/agents/twitter_outreach"
|
||||
agent_work_dir: str, # e.g., "~/.hive/agents/deep_research_agent"
|
||||
status: str = "", # "needs_attention", "success", "failure", "degraded"
|
||||
limit: int = 20
|
||||
) -> dict # {"runs": [...], "total": int}
|
||||
@@ -371,14 +371,14 @@ query_runtime_log_raw(agent_work_dir, run_id)
|
||||
```python
|
||||
# 1. Find problematic runs (L1)
|
||||
result = query_runtime_logs(
|
||||
agent_work_dir="~/.hive/agents/twitter_outreach",
|
||||
agent_work_dir="~/.hive/agents/deep_research_agent",
|
||||
status="needs_attention"
|
||||
)
|
||||
run_id = result["runs"][0]["run_id"]
|
||||
|
||||
# 2. Identify failing nodes (L2)
|
||||
details = query_runtime_log_details(
|
||||
agent_work_dir="~/.hive/agents/twitter_outreach",
|
||||
agent_work_dir="~/.hive/agents/deep_research_agent",
|
||||
run_id=run_id,
|
||||
needs_attention_only=True
|
||||
)
|
||||
@@ -386,7 +386,7 @@ problem_node = details["nodes"][0]["node_id"]
|
||||
|
||||
# 3. Analyze root cause (L3)
|
||||
raw = query_runtime_log_raw(
|
||||
agent_work_dir="~/.hive/agents/twitter_outreach",
|
||||
agent_work_dir="~/.hive/agents/deep_research_agent",
|
||||
run_id=run_id,
|
||||
node_id=problem_node
|
||||
)
|
||||
@@ -496,7 +496,7 @@ logger.start_run(goal_id, session_id=execution_id)
|
||||
```json
|
||||
{
|
||||
"run_id": "session_20260206_115718_e22339c5",
|
||||
"goal_id": "twitter-outreach-multi-loop",
|
||||
"goal_id": "deep-research",
|
||||
"status": "degraded",
|
||||
"started_at": "2026-02-06T11:57:18.593081",
|
||||
"ended_at": "2026-02-06T11:58:45.123456",
|
||||
|
||||
@@ -37,7 +37,7 @@ class SessionStore:
|
||||
Initialize session store.
|
||||
|
||||
Args:
|
||||
base_path: Base path for storage (e.g., ~/.hive/agents/twitter_outreach)
|
||||
base_path: Base path for storage (e.g., ~/.hive/agents/deep_research_agent)
|
||||
"""
|
||||
self.base_path = Path(base_path)
|
||||
self.sessions_dir = self.base_path / "sessions"
|
||||
|
||||
@@ -82,8 +82,10 @@ class ChatRepl(Vertical):
|
||||
self._streaming_snapshot: str = ""
|
||||
self._waiting_for_input: bool = False
|
||||
self._input_node_id: str | None = None
|
||||
self._pending_ask_question: str = ""
|
||||
self._resume_session = resume_session
|
||||
self._resume_checkpoint = resume_checkpoint
|
||||
self._session_index: list[str] = [] # Ordered session IDs from last /sessions or /resume listing
|
||||
|
||||
# Dedicated event loop for agent execution.
|
||||
# Keeps blocking runtime code (LLM calls, MCP tools) off
|
||||
@@ -138,8 +140,9 @@ class ChatRepl(Vertical):
|
||||
self._write_history("""[bold cyan]Available Commands:[/bold cyan]
|
||||
[bold]/sessions[/bold] - List all sessions for this agent
|
||||
[bold]/sessions[/bold] <session_id> - Show session details and checkpoints
|
||||
[bold]/resume[/bold] - Resume latest paused/failed session
|
||||
[bold]/resume[/bold] <session_id> - Resume session from where it stopped
|
||||
[bold]/resume[/bold] - List sessions and pick one to resume
|
||||
[bold]/resume[/bold] <number> - Resume session by list number
|
||||
[bold]/resume[/bold] <session_id> - Resume session by ID
|
||||
[bold]/recover[/bold] <session_id> <cp_id> - Recover from specific checkpoint
|
||||
[bold]/pause[/bold] - Pause current execution (Ctrl+Z)
|
||||
[bold]/help[/bold] - Show this help message
|
||||
@@ -147,8 +150,9 @@ class ChatRepl(Vertical):
|
||||
[dim]Examples:[/dim]
|
||||
/sessions [dim]# List all sessions[/dim]
|
||||
/sessions session_20260208_143022 [dim]# Show session details[/dim]
|
||||
/resume [dim]# Resume latest session (from state)[/dim]
|
||||
/resume session_20260208_143022 [dim]# Resume specific session (from state)[/dim]
|
||||
/resume [dim]# Show numbered session list[/dim]
|
||||
/resume 1 [dim]# Resume first listed session[/dim]
|
||||
/resume session_20260208_143022 [dim]# Resume by full session ID[/dim]
|
||||
/recover session_20260208_143022 cp_xxx [dim]# Recover from specific checkpoint[/dim]
|
||||
/pause [dim]# Pause (or Ctrl+Z)[/dim]
|
||||
""")
|
||||
@@ -156,15 +160,29 @@ class ChatRepl(Vertical):
|
||||
session_id = parts[1].strip() if len(parts) > 1 else None
|
||||
await self._cmd_sessions(session_id)
|
||||
elif cmd == "/resume":
|
||||
# Resume from session state (not checkpoint-based)
|
||||
if len(parts) < 2:
|
||||
session_id = await self._find_latest_resumable_session()
|
||||
if not session_id:
|
||||
self._write_history("[bold red]No resumable sessions found[/bold red]")
|
||||
self._write_history(" Tip: Use [bold]/sessions[/bold] to see all sessions")
|
||||
# No arg → show session list so user can pick one
|
||||
await self._cmd_sessions(None)
|
||||
return
|
||||
|
||||
arg = parts[1].strip()
|
||||
|
||||
# Numeric index → resolve from last listing
|
||||
if arg.isdigit():
|
||||
idx = int(arg) - 1 # 1-based to 0-based
|
||||
if 0 <= idx < len(self._session_index):
|
||||
session_id = self._session_index[idx]
|
||||
else:
|
||||
self._write_history(
|
||||
f"[bold red]Error:[/bold red] No session at index {arg}"
|
||||
)
|
||||
self._write_history(
|
||||
" Use [bold]/resume[/bold] to see available sessions"
|
||||
)
|
||||
return
|
||||
else:
|
||||
session_id = parts[1].strip()
|
||||
session_id = arg
|
||||
|
||||
await self._cmd_resume(session_id)
|
||||
elif cmd == "/recover":
|
||||
# Recover from specific checkpoint
|
||||
@@ -241,6 +259,15 @@ class ChatRepl(Vertical):
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _get_session_label(self, state: dict) -> str:
|
||||
"""Extract the first user message from input_data as a human-readable label."""
|
||||
input_data = state.get("input_data", {})
|
||||
for value in input_data.values():
|
||||
if isinstance(value, str) and value.strip():
|
||||
label = value.strip()
|
||||
return label[:60] + "..." if len(label) > 60 else label
|
||||
return "(no input)"
|
||||
|
||||
async def _list_sessions(self, storage_path: Path) -> None:
|
||||
"""List all sessions for the agent."""
|
||||
self._write_history("[bold cyan]Available Sessions:[/bold cyan]")
|
||||
@@ -264,6 +291,11 @@ class ChatRepl(Vertical):
|
||||
|
||||
self._write_history(f"[dim]Found {len(session_dirs)} session(s)[/dim]\n")
|
||||
|
||||
# Reset the session index for numeric lookups
|
||||
self._session_index = []
|
||||
|
||||
import json
|
||||
|
||||
for session_dir in session_dirs[:10]: # Show last 10 sessions
|
||||
session_id = session_dir.name
|
||||
state_file = session_dir / "state.json"
|
||||
@@ -273,12 +305,15 @@ class ChatRepl(Vertical):
|
||||
|
||||
# Read session state
|
||||
try:
|
||||
import json
|
||||
|
||||
with open(state_file) as f:
|
||||
state = json.load(f)
|
||||
|
||||
# Track this session for /resume <number> lookup
|
||||
self._session_index.append(session_id)
|
||||
index = len(self._session_index)
|
||||
|
||||
status = state.get("status", "unknown").upper()
|
||||
label = self._get_session_label(state)
|
||||
|
||||
# Status with color
|
||||
if status == "COMPLETED":
|
||||
@@ -292,25 +327,21 @@ class ChatRepl(Vertical):
|
||||
else:
|
||||
status_colored = f"[dim]{status}[/dim]"
|
||||
|
||||
# Check for checkpoints
|
||||
checkpoint_dir = session_dir / "checkpoints"
|
||||
checkpoint_count = 0
|
||||
if checkpoint_dir.exists():
|
||||
checkpoint_files = list(checkpoint_dir.glob("cp_*.json"))
|
||||
checkpoint_count = len(checkpoint_files)
|
||||
|
||||
# Session line
|
||||
self._write_history(f"📋 [bold]{session_id}[/bold]")
|
||||
self._write_history(f" Status: {status_colored} Checkpoints: {checkpoint_count}")
|
||||
|
||||
if checkpoint_count > 0:
|
||||
self._write_history(f" [dim]Resume: /resume {session_id}[/dim]")
|
||||
|
||||
# Session line with index and label
|
||||
self._write_history(
|
||||
f" [bold]{index}.[/bold] {label} {status_colored}"
|
||||
)
|
||||
self._write_history(f" [dim]{session_id}[/dim]")
|
||||
self._write_history("") # Blank line
|
||||
|
||||
except Exception as e:
|
||||
self._write_history(f" [dim red]Error reading: {e}[/dim red]")
|
||||
|
||||
if self._session_index:
|
||||
self._write_history(
|
||||
"[dim]Use [bold]/resume <number>[/bold] to resume a session[/dim]"
|
||||
)
|
||||
|
||||
async def _show_session_details(self, storage_path: Path, session_id: str) -> None:
|
||||
"""Show detailed information about a specific session."""
|
||||
self._write_history(f"[bold cyan]Session Details:[/bold cyan] {session_id}\n")
|
||||
@@ -682,16 +713,20 @@ class ChatRepl(Vertical):
|
||||
{
|
||||
"session_id": session_dir.name,
|
||||
"status": status.upper(),
|
||||
"label": self._get_session_label(state),
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if resumable:
|
||||
self._write_history("\n[bold yellow]⚠ Non-terminated sessions found:[/bold yellow]")
|
||||
# Populate session index so /resume <number> works immediately
|
||||
self._session_index = [s["session_id"] for s in resumable[:3]]
|
||||
|
||||
self._write_history("\n[bold yellow]Non-terminated sessions found:[/bold yellow]")
|
||||
for i, session in enumerate(resumable[:3], 1): # Show top 3
|
||||
status = session["status"]
|
||||
session_id = session["session_id"]
|
||||
label = session["label"]
|
||||
|
||||
# Color code status
|
||||
if status == "PAUSED":
|
||||
@@ -703,15 +738,12 @@ class ChatRepl(Vertical):
|
||||
else:
|
||||
status_colored = f"[dim]{status}[/dim]"
|
||||
|
||||
self._write_history(f" {i}. {session_id[:32]}... [{status_colored}]")
|
||||
self._write_history(f" [bold]{i}.[/bold] {label} {status_colored}")
|
||||
|
||||
self._write_history("\n[bold cyan]What would you like to do?[/bold cyan]")
|
||||
self._write_history(" • Type [bold]/resume[/bold] to continue the latest session")
|
||||
self._write_history(
|
||||
f" • Type [bold]/resume {resumable[0]['session_id']}[/bold] "
|
||||
"for specific session"
|
||||
"\n Type [bold]/resume <number>[/bold] to continue a session"
|
||||
)
|
||||
self._write_history(" • Or just type your input to start a new session\n")
|
||||
self._write_history(" Or just type your input to start a new session\n")
|
||||
|
||||
except Exception:
|
||||
# Silently fail - don't block TUI startup
|
||||
@@ -833,8 +865,16 @@ class ChatRepl(Vertical):
|
||||
|
||||
def handle_tool_started(self, tool_name: str, tool_input: dict[str, Any]) -> None:
|
||||
"""Handle a tool call starting."""
|
||||
# Update indicator to show tool activity
|
||||
indicator = self.query_one("#processing-indicator", Label)
|
||||
|
||||
if tool_name == "ask_user":
|
||||
# Stash the question for handle_input_requested() to display.
|
||||
# Suppress the generic "Tool: ask_user" line.
|
||||
self._pending_ask_question = tool_input.get("question", "")
|
||||
indicator.update("Preparing question...")
|
||||
return
|
||||
|
||||
# Update indicator to show tool activity
|
||||
indicator.update(f"Using tool: {tool_name}...")
|
||||
|
||||
# Write a discrete status line to history
|
||||
@@ -842,6 +882,11 @@ class ChatRepl(Vertical):
|
||||
|
||||
def handle_tool_completed(self, tool_name: str, result: str, is_error: bool) -> None:
|
||||
"""Handle a tool call completing."""
|
||||
if tool_name == "ask_user":
|
||||
# Suppress the synthetic "Waiting for user input..." result.
|
||||
# The actual question is displayed by handle_input_requested().
|
||||
return
|
||||
|
||||
result_str = str(result)
|
||||
preview = result_str[:200] + "..." if len(result_str) > 200 else result_str
|
||||
preview = preview.replace("\n", " ")
|
||||
@@ -872,6 +917,7 @@ class ChatRepl(Vertical):
|
||||
self._streaming_snapshot = ""
|
||||
self._waiting_for_input = False
|
||||
self._input_node_id = None
|
||||
self._pending_ask_question = ""
|
||||
|
||||
# Re-enable input
|
||||
chat_input = self.query_one("#chat-input", Input)
|
||||
@@ -890,6 +936,7 @@ class ChatRepl(Vertical):
|
||||
self._current_exec_id = None
|
||||
self._streaming_snapshot = ""
|
||||
self._waiting_for_input = False
|
||||
self._pending_ask_question = ""
|
||||
self._input_node_id = None
|
||||
|
||||
# Re-enable input
|
||||
@@ -906,10 +953,18 @@ class ChatRepl(Vertical):
|
||||
and sets a flag so the next submission routes to inject_input().
|
||||
"""
|
||||
# Flush accumulated streaming text as agent output
|
||||
if self._streaming_snapshot:
|
||||
self._write_history(f"[bold blue]Agent:[/bold blue] {self._streaming_snapshot}")
|
||||
flushed_snapshot = self._streaming_snapshot
|
||||
if flushed_snapshot:
|
||||
self._write_history(f"[bold blue]Agent:[/bold blue] {flushed_snapshot}")
|
||||
self._streaming_snapshot = ""
|
||||
|
||||
# Display the ask_user question if stashed and not already
|
||||
# present in the streaming snapshot (avoids double-display).
|
||||
question = self._pending_ask_question
|
||||
self._pending_ask_question = ""
|
||||
if question and question not in flushed_snapshot:
|
||||
self._write_history(f"[bold blue]Agent:[/bold blue] {question}")
|
||||
|
||||
self._waiting_for_input = True
|
||||
self._input_node_id = node_id or None
|
||||
|
||||
|
||||
@@ -32,26 +32,26 @@ Each goal has weighted success criteria that define what "done" looks like. Thes
|
||||
|
||||
```python
|
||||
Goal(
|
||||
id="twitter-outreach",
|
||||
name="Personalized Twitter Outreach",
|
||||
id="deep-research",
|
||||
name="Deep Research Report",
|
||||
success_criteria=[
|
||||
SuccessCriterion(
|
||||
id="personalized",
|
||||
description="Messages reference specific details from the prospect's profile",
|
||||
id="comprehensive",
|
||||
description="Report covers all major aspects of the research topic",
|
||||
metric="llm_judge",
|
||||
weight=0.4
|
||||
),
|
||||
SuccessCriterion(
|
||||
id="compliant",
|
||||
description="Messages follow brand voice guidelines",
|
||||
id="cited",
|
||||
description="All claims are backed by cited sources",
|
||||
metric="llm_judge",
|
||||
weight=0.3
|
||||
),
|
||||
SuccessCriterion(
|
||||
id="actionable",
|
||||
description="Each message includes a clear call to action",
|
||||
id="structured",
|
||||
description="Report has clear sections with headings and a summary",
|
||||
metric="output_contains",
|
||||
target="CTA",
|
||||
target="## Summary",
|
||||
weight=0.3
|
||||
),
|
||||
],
|
||||
|
||||
@@ -44,4 +44,3 @@ uv run python -m exports.my_research_agent --input '{"topic": "..."}'
|
||||
|----------|-------------|
|
||||
| [deep_research_agent](deep_research_agent/) | Interactive research agent that searches diverse sources, evaluates findings with user checkpoints, and produces a cited HTML report |
|
||||
| [tech_news_reporter](tech_news_reporter/) | Researches the latest technology and AI news from the web and produces a well-organized report |
|
||||
| [twitter_outreach](twitter_outreach/) | Researches a Twitter/X profile, crafts a personalized outreach email, and sends it after user approval |
|
||||
|
||||
@@ -69,10 +69,22 @@ You do NOT have web search — instead, scrape news directly from known sites.
|
||||
- Recency (past week)
|
||||
- Significance and diversity of topics
|
||||
|
||||
CRITICAL: Copy URLs EXACTLY as they appear in the "href" field of the scraped
|
||||
links. Do NOT reconstruct, guess, or modify URLs from memory. Use the verbatim
|
||||
href value from the web_scrape result.
|
||||
|
||||
3. For each selected article, use web_scrape with max_length=3000 on the
|
||||
individual article URL to get the content. Extract: title, source name,
|
||||
URL, publication date, a 2-3 sentence summary, and the main topic category.
|
||||
|
||||
4. **VERIFY LINKS** — Before producing your final output, verify each article URL
|
||||
by checking the web_scrape result you got in step 3:
|
||||
- If the scrape returned content successfully, the URL is verified — use it as-is.
|
||||
- If the scrape returned an error or the page was not found (404, timeout, etc.),
|
||||
go back to the front page links from step 1 and pick a different article URL
|
||||
to replace it. Scrape the replacement to confirm it works.
|
||||
- Only include articles whose URLs returned successful scrape results.
|
||||
|
||||
**Output format:**
|
||||
Use set_output("articles_data", <JSON string>) with this structure:
|
||||
```json
|
||||
@@ -94,12 +106,13 @@ Use set_output("articles_data", <JSON string>) with this structure:
|
||||
|
||||
**Rules:**
|
||||
- Only include REAL articles with REAL URLs you scraped. Never fabricate.
|
||||
- The "url" field MUST be a URL you successfully scraped. Never invent URLs.
|
||||
- Focus on news from the past week.
|
||||
- Aim for at least 3 distinct topic categories.
|
||||
- Keep summaries factual and concise.
|
||||
- If a site fails to load, skip it and move on to the next.
|
||||
- Always use max_length to limit scraped content (5000 for front pages, 3000 for articles).
|
||||
- Work in batches: scrape front pages first, then articles. Don't scrape everything at once.
|
||||
- Work in batches: scrape front pages first, then articles, then verify. Don't scrape everything at once.
|
||||
""",
|
||||
tools=["web_scrape"],
|
||||
)
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# Twitter Outreach Agent
|
||||
|
||||
Personalized email outreach powered by Twitter/X research.
|
||||
|
||||
## What it does
|
||||
|
||||
1. **Intake** — Collects the target's Twitter handle, outreach purpose, and recipient email
|
||||
2. **Research** — Searches and scrapes the target's Twitter/X profile for bio, tweets, interests
|
||||
3. **Draft & Review** — Crafts a personalized email and presents it for your approval (with iteration)
|
||||
4. **Send** — Sends the approved email
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Validate the agent structure
|
||||
PYTHONPATH=core:exports uv run python -m twitter_outreach validate
|
||||
|
||||
# Show agent info
|
||||
PYTHONPATH=core:exports uv run python -m twitter_outreach info
|
||||
|
||||
# Run the workflow
|
||||
PYTHONPATH=core:exports uv run python -m twitter_outreach run
|
||||
|
||||
# Launch the TUI
|
||||
PYTHONPATH=core:exports uv run python -m twitter_outreach tui
|
||||
|
||||
# Interactive shell
|
||||
PYTHONPATH=core:exports uv run python -m twitter_outreach shell
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
intake → research → draft-review → send
|
||||
```
|
||||
|
||||
## Tools Used
|
||||
|
||||
- `web_search` — Search for Twitter profiles and public info
|
||||
- `web_scrape` — Read Twitter/X profile pages
|
||||
- `send_email` — Send the approved outreach email
|
||||
|
||||
## Nodes
|
||||
|
||||
| Node | Type | Client-Facing | Description |
|
||||
|------|------|:---:|-------------|
|
||||
| `intake` | event_loop | Yes | Collect target info from user |
|
||||
| `research` | event_loop | No | Research Twitter/X profile |
|
||||
| `draft-review` | event_loop | Yes | Draft email, iterate with user |
|
||||
| `send` | event_loop | No | Send approved email |
|
||||
|
||||
## Constraints
|
||||
|
||||
- **No Spam** — No spammy language, clickbait, or aggressive sales tactics
|
||||
- **Approval Required** — Never sends without explicit user approval
|
||||
- **Tone** — Professional, authentic, conversational
|
||||
- **Privacy** — Only uses publicly available information
|
||||
@@ -1,23 +0,0 @@
|
||||
"""
|
||||
Twitter Outreach Agent - Personalized email outreach powered by Twitter/X research.
|
||||
|
||||
Reads a target's Twitter/X profile, crafts a personalized outreach email
|
||||
referencing their specific activity, and sends it after user approval.
|
||||
"""
|
||||
|
||||
from .agent import TwitterOutreachAgent, default_agent, goal, nodes, edges
|
||||
from .config import RuntimeConfig, AgentMetadata, default_config, metadata
|
||||
|
||||
__version__ = "1.0.0"
|
||||
|
||||
__all__ = [
|
||||
"TwitterOutreachAgent",
|
||||
"default_agent",
|
||||
"goal",
|
||||
"nodes",
|
||||
"edges",
|
||||
"RuntimeConfig",
|
||||
"AgentMetadata",
|
||||
"default_config",
|
||||
"metadata",
|
||||
]
|
||||
@@ -1,206 +0,0 @@
|
||||
"""
|
||||
CLI entry point for Twitter Outreach Agent.
|
||||
|
||||
Uses AgentRuntime for TUI support with client-facing interaction.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import click
|
||||
|
||||
from .agent import default_agent, TwitterOutreachAgent
|
||||
|
||||
|
||||
def setup_logging(verbose=False, debug=False):
|
||||
"""Configure logging for execution visibility."""
|
||||
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)
|
||||
logging.getLogger("framework").setLevel(level)
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version="1.0.0")
|
||||
def cli():
|
||||
"""Twitter Outreach Agent - Personalized email outreach powered by Twitter/X research."""
|
||||
pass
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--quiet", "-q", is_flag=True, help="Only output result JSON")
|
||||
@click.option("--verbose", "-v", is_flag=True, help="Show execution details")
|
||||
@click.option("--debug", is_flag=True, help="Show debug logging")
|
||||
def run(quiet, verbose, debug):
|
||||
"""Execute the outreach workflow."""
|
||||
if not quiet:
|
||||
setup_logging(verbose=verbose, debug=debug)
|
||||
|
||||
result = asyncio.run(default_agent.run({}))
|
||||
|
||||
output_data = {
|
||||
"success": result.success,
|
||||
"steps_executed": result.steps_executed,
|
||||
"output": result.output,
|
||||
}
|
||||
if result.error:
|
||||
output_data["error"] = result.error
|
||||
|
||||
click.echo(json.dumps(output_data, indent=2, default=str))
|
||||
sys.exit(0 if result.success else 1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--verbose", "-v", is_flag=True, help="Show execution details")
|
||||
@click.option("--debug", is_flag=True, help="Show debug logging")
|
||||
def tui(verbose, debug):
|
||||
"""Launch the TUI dashboard for interactive outreach."""
|
||||
setup_logging(verbose=verbose, debug=debug)
|
||||
|
||||
try:
|
||||
from framework.tui.app import AdenTUI
|
||||
except ImportError:
|
||||
click.echo(
|
||||
"TUI requires the 'textual' package. Install with: pip install textual"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from framework.llm import LiteLLMProvider
|
||||
from framework.runner.tool_registry import ToolRegistry
|
||||
from framework.runtime.agent_runtime import create_agent_runtime
|
||||
from framework.runtime.event_bus import EventBus
|
||||
from framework.runtime.execution_stream import EntryPointSpec
|
||||
|
||||
async def run_with_tui():
|
||||
agent = TwitterOutreachAgent()
|
||||
|
||||
agent._event_bus = EventBus()
|
||||
agent._tool_registry = ToolRegistry()
|
||||
|
||||
storage_path = Path.home() / ".hive" / "twitter_outreach"
|
||||
storage_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
mcp_config_path = Path(__file__).parent / "mcp_servers.json"
|
||||
if mcp_config_path.exists():
|
||||
agent._tool_registry.load_mcp_config(mcp_config_path)
|
||||
|
||||
llm = LiteLLMProvider(
|
||||
model=agent.config.model,
|
||||
api_key=agent.config.api_key,
|
||||
api_base=agent.config.api_base,
|
||||
)
|
||||
|
||||
tools = list(agent._tool_registry.get_tools().values())
|
||||
tool_executor = agent._tool_registry.get_executor()
|
||||
graph = agent._build_graph()
|
||||
|
||||
runtime = create_agent_runtime(
|
||||
graph=graph,
|
||||
goal=agent.goal,
|
||||
storage_path=storage_path,
|
||||
entry_points=[
|
||||
EntryPointSpec(
|
||||
id="start",
|
||||
name="Start Outreach",
|
||||
entry_node="intake",
|
||||
trigger_type="manual",
|
||||
isolation_level="isolated",
|
||||
),
|
||||
],
|
||||
llm=llm,
|
||||
tools=tools,
|
||||
tool_executor=tool_executor,
|
||||
)
|
||||
|
||||
await runtime.start()
|
||||
|
||||
try:
|
||||
app = AdenTUI(runtime)
|
||||
await app.run_async()
|
||||
finally:
|
||||
await runtime.stop()
|
||||
|
||||
asyncio.run(run_with_tui())
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--json", "output_json", is_flag=True)
|
||||
def info(output_json):
|
||||
"""Show agent information."""
|
||||
info_data = default_agent.info()
|
||||
if output_json:
|
||||
click.echo(json.dumps(info_data, indent=2))
|
||||
else:
|
||||
click.echo(f"Agent: {info_data['name']}")
|
||||
click.echo(f"Version: {info_data['version']}")
|
||||
click.echo(f"Description: {info_data['description']}")
|
||||
click.echo(f"\nNodes: {', '.join(info_data['nodes'])}")
|
||||
click.echo(f"Client-facing: {', '.join(info_data['client_facing_nodes'])}")
|
||||
click.echo(f"Entry: {info_data['entry_node']}")
|
||||
click.echo(f"Terminal: {', '.join(info_data['terminal_nodes'])}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
def validate():
|
||||
"""Validate agent structure."""
|
||||
validation = default_agent.validate()
|
||||
if validation["valid"]:
|
||||
click.echo("Agent is valid")
|
||||
if validation["warnings"]:
|
||||
for warning in validation["warnings"]:
|
||||
click.echo(f" WARNING: {warning}")
|
||||
else:
|
||||
click.echo("Agent has errors:")
|
||||
for error in validation["errors"]:
|
||||
click.echo(f" ERROR: {error}")
|
||||
sys.exit(0 if validation["valid"] else 1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--verbose", "-v", is_flag=True)
|
||||
def shell(verbose):
|
||||
"""Interactive outreach session (CLI, no TUI)."""
|
||||
asyncio.run(_interactive_shell(verbose))
|
||||
|
||||
|
||||
async def _interactive_shell(verbose=False):
|
||||
"""Async interactive shell."""
|
||||
setup_logging(verbose=verbose)
|
||||
|
||||
click.echo("=== Twitter Outreach Agent ===")
|
||||
click.echo("Starting outreach workflow...\n")
|
||||
|
||||
agent = TwitterOutreachAgent()
|
||||
await agent.start()
|
||||
|
||||
try:
|
||||
result = await agent.trigger_and_wait("start", {})
|
||||
|
||||
if result is None:
|
||||
click.echo("\n[Execution timed out]\n")
|
||||
elif result.success:
|
||||
output = result.output
|
||||
status = output.get("delivery_status", "unknown")
|
||||
click.echo(f"\nOutreach complete! Delivery status: {status}")
|
||||
else:
|
||||
click.echo(f"\nOutreach failed: {result.error}")
|
||||
except KeyboardInterrupt:
|
||||
click.echo("\nGoodbye!")
|
||||
except Exception as e:
|
||||
click.echo(f"Error: {e}", err=True)
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
await agent.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
@@ -1,265 +0,0 @@
|
||||
{
|
||||
"agent": {
|
||||
"id": "twitter_outreach",
|
||||
"name": "Personalized Twitter Outreach",
|
||||
"version": "1.0.0",
|
||||
"description": "Given a Twitter/X handle and outreach context, research the target's profile (bio, tweets, interests), craft a personalized outreach email referencing their specific activity, and send it after user approval."
|
||||
},
|
||||
"graph": {
|
||||
"id": "twitter_outreach-graph",
|
||||
"goal_id": "twitter-outreach",
|
||||
"version": "1.0.0",
|
||||
"entry_node": "intake",
|
||||
"entry_points": {
|
||||
"start": "intake"
|
||||
},
|
||||
"pause_nodes": [],
|
||||
"terminal_nodes": [
|
||||
"send"
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "intake",
|
||||
"name": "Intake",
|
||||
"description": "Collect the target Twitter handle, outreach purpose, and recipient email from the user",
|
||||
"node_type": "event_loop",
|
||||
"input_keys": [],
|
||||
"output_keys": [
|
||||
"twitter_handle",
|
||||
"outreach_context",
|
||||
"recipient_email"
|
||||
],
|
||||
"nullable_output_keys": [],
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"system_prompt": "You are the intake assistant for a personalized Twitter outreach agent.\n\n**STEP 1 \u2014 Respond to the user (text only, NO tool calls):**\nGreet the user and ask them to provide:\n1. The Twitter/X handle of the person they want to reach out to\n2. The purpose/context of the outreach (e.g., partnership opportunity, hiring, collaboration, introduction)\n3. The recipient's email address\n\nBe friendly and concise. If the user provides partial info, ask for what's missing.\n\n**STEP 2 \u2014 After the user provides ALL three pieces of information, call set_output:**\n- set_output(\"twitter_handle\", \"<the Twitter handle, including @>\")\n- set_output(\"outreach_context\", \"<the outreach purpose/context>\")\n- set_output(\"recipient_email\", \"<the email address>\")",
|
||||
"tools": [],
|
||||
"model": null,
|
||||
"function": null,
|
||||
"routes": {},
|
||||
"max_retries": 3,
|
||||
"retry_on": [],
|
||||
"max_node_visits": 1,
|
||||
"output_model": null,
|
||||
"max_validation_retries": 2,
|
||||
"client_facing": true
|
||||
},
|
||||
{
|
||||
"id": "research",
|
||||
"name": "Research",
|
||||
"description": "Research the target's Twitter/X profile \u2014 bio, recent tweets, interests, and topics they engage with",
|
||||
"node_type": "event_loop",
|
||||
"input_keys": [
|
||||
"twitter_handle"
|
||||
],
|
||||
"output_keys": [
|
||||
"profile_summary"
|
||||
],
|
||||
"nullable_output_keys": [],
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"system_prompt": "You are a Twitter/X profile researcher. Your job is to thoroughly research a person's public Twitter/X presence.\n\nGiven the Twitter handle provided in your inputs, do the following:\n\n1. Use web_search to find their Twitter/X profile and any relevant public information about them.\n2. Use web_scrape to read their Twitter/X profile page (try https://x.com/{handle} or https://twitter.com/{handle}).\n3. Extract and analyze:\n - Their bio and self-description\n - Recent tweets and topics they post about\n - Professional interests, projects, or accomplishments\n - Any recurring themes or passions\n - Specific tweets worth referencing in outreach\n4. Look for additional context (personal website, blog, other social profiles mentioned in bio).\n\nCompile a comprehensive profile summary that would help someone write a highly personalized outreach email.\n\nUse set_output(\"profile_summary\", <your detailed summary as a string>) to store your findings.\n\nDo NOT return raw JSON. Use the set_output tool to produce outputs.",
|
||||
"tools": [
|
||||
"web_search",
|
||||
"web_scrape"
|
||||
],
|
||||
"model": null,
|
||||
"function": null,
|
||||
"routes": {},
|
||||
"max_retries": 3,
|
||||
"retry_on": [],
|
||||
"max_node_visits": 1,
|
||||
"output_model": null,
|
||||
"max_validation_retries": 2,
|
||||
"client_facing": false
|
||||
},
|
||||
{
|
||||
"id": "draft-review",
|
||||
"name": "Draft & Review",
|
||||
"description": "Draft a personalized outreach email using profile research, present to user for review, and iterate until approved",
|
||||
"node_type": "event_loop",
|
||||
"input_keys": [
|
||||
"outreach_context",
|
||||
"recipient_email",
|
||||
"profile_summary"
|
||||
],
|
||||
"output_keys": [
|
||||
"approved_email"
|
||||
],
|
||||
"nullable_output_keys": [],
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"system_prompt": "You are an expert email copywriter specializing in personalized outreach.\n\nYou have been given:\n- A profile summary of the target person (from their Twitter/X)\n- The outreach context/purpose\n- The recipient's email address\n\n**STEP 1 \u2014 Draft and present the email (text only, NO tool calls):**\n\nUsing the profile research, draft a personalized outreach email that:\n- References at least 2 specific details from their Twitter profile (tweets, interests, projects)\n- Clearly connects to the outreach purpose\n- Includes a specific, relevant call to action\n- Is professional but conversational and authentic \u2014 NOT spammy, robotic, or overly formal\n- Is concise (under 300 words)\n\nPresent the complete email draft to the user, formatted clearly with Subject line and Body.\nThen ask: \"Would you like any changes, or shall I send this?\"\n\nIf the user requests changes, revise the email and present the updated version. Keep iterating until the user is satisfied.\n\n**STEP 2 \u2014 After the user explicitly approves the email, call set_output:**\n- set_output(\"approved_email\", \"<the final approved email text including subject line>\")",
|
||||
"tools": [],
|
||||
"model": null,
|
||||
"function": null,
|
||||
"routes": {},
|
||||
"max_retries": 3,
|
||||
"retry_on": [],
|
||||
"max_node_visits": 1,
|
||||
"output_model": null,
|
||||
"max_validation_retries": 2,
|
||||
"client_facing": true
|
||||
},
|
||||
{
|
||||
"id": "send",
|
||||
"name": "Send",
|
||||
"description": "Send the approved outreach email to the recipient",
|
||||
"node_type": "event_loop",
|
||||
"input_keys": [
|
||||
"approved_email",
|
||||
"recipient_email"
|
||||
],
|
||||
"output_keys": [
|
||||
"delivery_status"
|
||||
],
|
||||
"nullable_output_keys": [],
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"system_prompt": "You are responsible for sending the approved outreach email.\n\nYou have the approved email text and the recipient's email address in your inputs.\n\nParse the subject line and body from the approved_email, then use the send_email tool to send it to the recipient_email address.\n\nAfter sending successfully, call:\n- set_output(\"delivery_status\", \"sent\")\n\nIf there is an error sending, call:\n- set_output(\"delivery_status\", \"failed: <error details>\")\n\nDo NOT return raw JSON. Use the set_output tool to produce outputs.",
|
||||
"tools": [
|
||||
"send_email"
|
||||
],
|
||||
"model": null,
|
||||
"function": null,
|
||||
"routes": {},
|
||||
"max_retries": 3,
|
||||
"retry_on": [],
|
||||
"max_node_visits": 1,
|
||||
"output_model": null,
|
||||
"max_validation_retries": 2,
|
||||
"client_facing": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "intake-to-research",
|
||||
"source": "intake",
|
||||
"target": "research",
|
||||
"condition": "on_success",
|
||||
"condition_expr": null,
|
||||
"priority": 1,
|
||||
"input_mapping": {}
|
||||
},
|
||||
{
|
||||
"id": "research-to-draft-review",
|
||||
"source": "research",
|
||||
"target": "draft-review",
|
||||
"condition": "on_success",
|
||||
"condition_expr": null,
|
||||
"priority": 1,
|
||||
"input_mapping": {}
|
||||
},
|
||||
{
|
||||
"id": "draft-review-to-send",
|
||||
"source": "draft-review",
|
||||
"target": "send",
|
||||
"condition": "on_success",
|
||||
"condition_expr": null,
|
||||
"priority": 1,
|
||||
"input_mapping": {}
|
||||
}
|
||||
],
|
||||
"max_steps": 100,
|
||||
"max_retries_per_node": 3,
|
||||
"description": "Given a Twitter/X handle and outreach context, research the target's profile (bio, tweets, interests), craft a personalized outreach email referencing their specific activity, and send it after user approval.",
|
||||
"created_at": "2026-02-05T13:32:44.573661"
|
||||
},
|
||||
"goal": {
|
||||
"id": "twitter-outreach",
|
||||
"name": "Personalized Twitter Outreach",
|
||||
"description": "Given a Twitter/X handle and outreach context, research the target's profile (bio, tweets, interests), craft a personalized outreach email referencing their specific activity, and send it after user approval.",
|
||||
"status": "draft",
|
||||
"success_criteria": [
|
||||
{
|
||||
"id": "profile-research",
|
||||
"description": "Agent extracts meaningful information from target's Twitter profile including bio, recent tweets, interests, and topics they engage with",
|
||||
"metric": "research_quality",
|
||||
"target": "Identifies at least 3 distinct profile details",
|
||||
"weight": 0.25,
|
||||
"met": false
|
||||
},
|
||||
{
|
||||
"id": "email-personalization",
|
||||
"description": "Drafted email references at least 2 specific details from the target's Twitter profile",
|
||||
"metric": "personalization_score",
|
||||
"target": "At least 2 specific references to profile content",
|
||||
"weight": 0.25,
|
||||
"met": false
|
||||
},
|
||||
{
|
||||
"id": "clear-cta",
|
||||
"description": "Email includes a specific relevant call to action",
|
||||
"metric": "cta_present",
|
||||
"target": "Email contains clear call to action",
|
||||
"weight": 0.15,
|
||||
"met": false
|
||||
},
|
||||
{
|
||||
"id": "user-approval-gate",
|
||||
"description": "Email is presented to user for review and only sent after explicit approval with opportunity to request edits",
|
||||
"metric": "approval_obtained",
|
||||
"target": "User explicitly approves before send",
|
||||
"weight": 0.2,
|
||||
"met": false
|
||||
},
|
||||
{
|
||||
"id": "successful-delivery",
|
||||
"description": "Email is sent successfully via the send_email tool",
|
||||
"metric": "delivery_status",
|
||||
"target": "Email sent without errors",
|
||||
"weight": 0.15,
|
||||
"met": false
|
||||
}
|
||||
],
|
||||
"constraints": [
|
||||
{
|
||||
"id": "no-spam",
|
||||
"description": "Email must not use spammy language, clickbait, or aggressive sales tactics",
|
||||
"constraint_type": "quality",
|
||||
"category": "content",
|
||||
"check": ""
|
||||
},
|
||||
{
|
||||
"id": "approval-required",
|
||||
"description": "Must never send an email without explicit user approval",
|
||||
"constraint_type": "safety",
|
||||
"category": "process",
|
||||
"check": ""
|
||||
},
|
||||
{
|
||||
"id": "tone-appropriate",
|
||||
"description": "Email tone must be professional, authentic, and conversational \u2014 not robotic or overly formal",
|
||||
"constraint_type": "quality",
|
||||
"category": "content",
|
||||
"check": ""
|
||||
},
|
||||
{
|
||||
"id": "privacy-respect",
|
||||
"description": "Only use publicly available information from the target's Twitter profile",
|
||||
"constraint_type": "safety",
|
||||
"category": "ethics",
|
||||
"check": ""
|
||||
}
|
||||
],
|
||||
"context": {},
|
||||
"required_capabilities": [],
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"version": "1.0.0",
|
||||
"parent_version": null,
|
||||
"evolution_reason": null,
|
||||
"created_at": "2026-02-05 13:30:59.934460",
|
||||
"updated_at": "2026-02-05 13:30:59.934462"
|
||||
},
|
||||
"required_tools": [
|
||||
"web_scrape",
|
||||
"send_email",
|
||||
"web_search"
|
||||
],
|
||||
"metadata": {
|
||||
"created_at": "2026-02-05T13:32:44.573712",
|
||||
"node_count": 4,
|
||||
"edge_count": 3
|
||||
}
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
"""Agent graph construction for Twitter Outreach Agent."""
|
||||
|
||||
from framework.graph import EdgeSpec, EdgeCondition, Goal, SuccessCriterion, Constraint
|
||||
from framework.graph.edge import GraphSpec
|
||||
from framework.graph.executor import ExecutionResult, GraphExecutor
|
||||
from framework.runtime.event_bus import EventBus
|
||||
from framework.runtime.core import Runtime
|
||||
from framework.llm import LiteLLMProvider
|
||||
from framework.runner.tool_registry import ToolRegistry
|
||||
|
||||
from .config import default_config, metadata
|
||||
from .nodes import (
|
||||
intake_node,
|
||||
research_node,
|
||||
draft_review_node,
|
||||
send_node,
|
||||
)
|
||||
|
||||
# Goal definition
|
||||
goal = Goal(
|
||||
id="twitter-outreach",
|
||||
name="Personalized Twitter Outreach",
|
||||
description=(
|
||||
"Given a Twitter/X handle and outreach context, research the target's profile "
|
||||
"(bio, tweets, interests), craft a personalized outreach email referencing their "
|
||||
"specific activity, and send it after user approval."
|
||||
),
|
||||
success_criteria=[
|
||||
SuccessCriterion(
|
||||
id="profile-research",
|
||||
description="Agent extracts meaningful information from target's Twitter profile including bio, recent tweets, interests, and topics they engage with",
|
||||
metric="research_quality",
|
||||
target="Identifies at least 3 distinct profile details",
|
||||
weight=0.25,
|
||||
),
|
||||
SuccessCriterion(
|
||||
id="email-personalization",
|
||||
description="Drafted email references at least 2 specific details from the target's Twitter profile",
|
||||
metric="personalization_score",
|
||||
target="At least 2 specific references to profile content",
|
||||
weight=0.25,
|
||||
),
|
||||
SuccessCriterion(
|
||||
id="clear-cta",
|
||||
description="Email includes a specific relevant call to action",
|
||||
metric="cta_present",
|
||||
target="Email contains clear call to action",
|
||||
weight=0.15,
|
||||
),
|
||||
SuccessCriterion(
|
||||
id="user-approval-gate",
|
||||
description="Email is presented to user for review and only sent after explicit approval with opportunity to request edits",
|
||||
metric="approval_obtained",
|
||||
target="User explicitly approves before send",
|
||||
weight=0.2,
|
||||
),
|
||||
SuccessCriterion(
|
||||
id="successful-delivery",
|
||||
description="Email is sent successfully via the send_email tool",
|
||||
metric="delivery_status",
|
||||
target="Email sent without errors",
|
||||
weight=0.15,
|
||||
),
|
||||
],
|
||||
constraints=[
|
||||
Constraint(
|
||||
id="no-spam",
|
||||
description="Email must not use spammy language, clickbait, or aggressive sales tactics",
|
||||
constraint_type="quality",
|
||||
category="content",
|
||||
),
|
||||
Constraint(
|
||||
id="approval-required",
|
||||
description="Must never send an email without explicit user approval",
|
||||
constraint_type="safety",
|
||||
category="process",
|
||||
),
|
||||
Constraint(
|
||||
id="tone-appropriate",
|
||||
description="Email tone must be professional, authentic, and conversational — not robotic or overly formal",
|
||||
constraint_type="quality",
|
||||
category="content",
|
||||
),
|
||||
Constraint(
|
||||
id="privacy-respect",
|
||||
description="Only use publicly available information from the target's Twitter profile",
|
||||
constraint_type="safety",
|
||||
category="ethics",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# Node list
|
||||
nodes = [
|
||||
intake_node,
|
||||
research_node,
|
||||
draft_review_node,
|
||||
send_node,
|
||||
]
|
||||
|
||||
# Edge definitions
|
||||
edges = [
|
||||
EdgeSpec(
|
||||
id="intake-to-research",
|
||||
source="intake",
|
||||
target="research",
|
||||
condition=EdgeCondition.ON_SUCCESS,
|
||||
priority=1,
|
||||
),
|
||||
EdgeSpec(
|
||||
id="research-to-draft-review",
|
||||
source="research",
|
||||
target="draft-review",
|
||||
condition=EdgeCondition.ON_SUCCESS,
|
||||
priority=1,
|
||||
),
|
||||
EdgeSpec(
|
||||
id="draft-review-to-send",
|
||||
source="draft-review",
|
||||
target="send",
|
||||
condition=EdgeCondition.ON_SUCCESS,
|
||||
priority=1,
|
||||
),
|
||||
]
|
||||
|
||||
# Graph configuration
|
||||
entry_node = "intake"
|
||||
entry_points = {"start": "intake"}
|
||||
pause_nodes = []
|
||||
terminal_nodes = ["send"]
|
||||
|
||||
|
||||
class TwitterOutreachAgent:
|
||||
"""
|
||||
Twitter Outreach Agent — 4-node pipeline with user approval checkpoint.
|
||||
|
||||
Flow: intake -> research -> draft-review -> send
|
||||
"""
|
||||
|
||||
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._executor: GraphExecutor | None = None
|
||||
self._graph: GraphSpec | None = None
|
||||
self._event_bus: EventBus | None = None
|
||||
self._tool_registry: ToolRegistry | None = None
|
||||
|
||||
def _build_graph(self) -> GraphSpec:
|
||||
"""Build the GraphSpec."""
|
||||
return GraphSpec(
|
||||
id="twitter-outreach-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={
|
||||
"max_iterations": 50,
|
||||
"max_tool_calls_per_turn": 10,
|
||||
"max_history_tokens": 32000,
|
||||
},
|
||||
)
|
||||
|
||||
def _setup(self) -> GraphExecutor:
|
||||
"""Set up the executor with all components."""
|
||||
from pathlib import Path
|
||||
|
||||
storage_path = Path.home() / ".hive" / "twitter_outreach"
|
||||
storage_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self._event_bus = EventBus()
|
||||
self._tool_registry = ToolRegistry()
|
||||
|
||||
mcp_config_path = Path(__file__).parent / "mcp_servers.json"
|
||||
if mcp_config_path.exists():
|
||||
self._tool_registry.load_mcp_config(mcp_config_path)
|
||||
|
||||
llm = LiteLLMProvider(
|
||||
model=self.config.model,
|
||||
api_key=self.config.api_key,
|
||||
api_base=self.config.api_base,
|
||||
)
|
||||
|
||||
tool_executor = self._tool_registry.get_executor()
|
||||
tools = list(self._tool_registry.get_tools().values())
|
||||
|
||||
self._graph = self._build_graph()
|
||||
runtime = Runtime(storage_path)
|
||||
|
||||
self._executor = GraphExecutor(
|
||||
runtime=runtime,
|
||||
llm=llm,
|
||||
tools=tools,
|
||||
tool_executor=tool_executor,
|
||||
event_bus=self._event_bus,
|
||||
storage_path=storage_path,
|
||||
loop_config=self._graph.loop_config,
|
||||
)
|
||||
|
||||
return self._executor
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Set up the agent (initialize executor and tools)."""
|
||||
if self._executor is None:
|
||||
self._setup()
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Clean up resources."""
|
||||
self._executor = None
|
||||
self._event_bus = None
|
||||
|
||||
async def trigger_and_wait(
|
||||
self,
|
||||
entry_point: str,
|
||||
input_data: dict,
|
||||
timeout: float | None = None,
|
||||
session_state: dict | None = None,
|
||||
) -> ExecutionResult | None:
|
||||
"""Execute the graph and wait for completion."""
|
||||
if self._executor is None:
|
||||
raise RuntimeError("Agent not started. Call start() first.")
|
||||
if self._graph is None:
|
||||
raise RuntimeError("Graph not built. Call start() first.")
|
||||
|
||||
return await self._executor.execute(
|
||||
graph=self._graph,
|
||||
goal=self.goal,
|
||||
input_data=input_data,
|
||||
session_state=session_state,
|
||||
)
|
||||
|
||||
async def run(self, context: dict, session_state=None) -> ExecutionResult:
|
||||
"""Run the agent (convenience method for single execution)."""
|
||||
await self.start()
|
||||
try:
|
||||
result = await self.trigger_and_wait(
|
||||
"start", context, session_state=session_state
|
||||
)
|
||||
return result or ExecutionResult(success=False, error="Execution timeout")
|
||||
finally:
|
||||
await self.stop()
|
||||
|
||||
def info(self):
|
||||
"""Get agent information."""
|
||||
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,
|
||||
"pause_nodes": self.pause_nodes,
|
||||
"terminal_nodes": self.terminal_nodes,
|
||||
"client_facing_nodes": [n.id for n in self.nodes if n.client_facing],
|
||||
}
|
||||
|
||||
def validate(self):
|
||||
"""Validate agent structure."""
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
node_ids = {node.id for node in self.nodes}
|
||||
for edge in self.edges:
|
||||
if edge.source not in node_ids:
|
||||
errors.append(f"Edge {edge.id}: source '{edge.source}' not found")
|
||||
if edge.target not in node_ids:
|
||||
errors.append(f"Edge {edge.id}: target '{edge.target}' not found")
|
||||
|
||||
if self.entry_node not in node_ids:
|
||||
errors.append(f"Entry node '{self.entry_node}' not found")
|
||||
|
||||
for terminal in self.terminal_nodes:
|
||||
if terminal not in node_ids:
|
||||
errors.append(f"Terminal node '{terminal}' not found")
|
||||
|
||||
for ep_id, node_id in self.entry_points.items():
|
||||
if node_id not in node_ids:
|
||||
errors.append(
|
||||
f"Entry point '{ep_id}' references unknown node '{node_id}'"
|
||||
)
|
||||
|
||||
return {
|
||||
"valid": len(errors) == 0,
|
||||
"errors": errors,
|
||||
"warnings": warnings,
|
||||
}
|
||||
|
||||
|
||||
# Create default instance
|
||||
default_agent = TwitterOutreachAgent()
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Runtime configuration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from framework.config import RuntimeConfig
|
||||
|
||||
default_config = RuntimeConfig()
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentMetadata:
|
||||
name: str = "Twitter Outreach Agent"
|
||||
version: str = "1.0.0"
|
||||
description: str = (
|
||||
"Reads a target's Twitter/X profile, crafts a personalized outreach email "
|
||||
"referencing their specific activity, and sends it after user approval."
|
||||
)
|
||||
intro_message: str = (
|
||||
"Hi! I can help you with personalized Twitter outreach. Give me a Twitter/X "
|
||||
"handle and I'll analyze their profile, then craft a tailored outreach email "
|
||||
"for your approval."
|
||||
)
|
||||
|
||||
|
||||
metadata = AgentMetadata()
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"hive-tools": {
|
||||
"transport": "stdio",
|
||||
"command": "uv",
|
||||
"args": ["run", "python", "mcp_server.py", "--stdio"],
|
||||
"cwd": "../../../tools",
|
||||
"description": "Hive tools MCP server providing web_search, web_scrape, and send_email"
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
"""Node definitions for Twitter Outreach Agent."""
|
||||
|
||||
from framework.graph import NodeSpec
|
||||
|
||||
# Node 1: Intake (client-facing)
|
||||
# Collect the target Twitter handle, outreach purpose, and recipient email.
|
||||
intake_node = NodeSpec(
|
||||
id="intake",
|
||||
name="Intake",
|
||||
description="Collect the target Twitter handle, outreach purpose, and recipient email from the user",
|
||||
node_type="event_loop",
|
||||
client_facing=True,
|
||||
input_keys=[],
|
||||
output_keys=["twitter_handle", "outreach_context", "recipient_email"],
|
||||
system_prompt="""\
|
||||
You are the intake assistant for a personalized Twitter outreach agent.
|
||||
|
||||
**STEP 1 — Respond to the user (text only, NO tool calls):**
|
||||
Greet the user and ask them to provide:
|
||||
1. The Twitter/X handle of the person they want to reach out to
|
||||
2. The purpose/context of the outreach (e.g., partnership opportunity, hiring, collaboration, introduction)
|
||||
3. The recipient's email address
|
||||
|
||||
Be friendly and concise. If the user provides partial info, ask for what's missing.
|
||||
|
||||
**STEP 2 — After the user provides ALL three pieces of information, call set_output:**
|
||||
- set_output("twitter_handle", "<the Twitter handle, including @>")
|
||||
- set_output("outreach_context", "<the outreach purpose/context>")
|
||||
- set_output("recipient_email", "<the email address>")
|
||||
""",
|
||||
tools=[],
|
||||
)
|
||||
|
||||
# Node 2: Research
|
||||
# Searches the web and scrapes the target's Twitter/X profile to build a comprehensive summary.
|
||||
research_node = NodeSpec(
|
||||
id="research",
|
||||
name="Research",
|
||||
description="Research the target's Twitter/X profile — bio, recent tweets, interests, and topics they engage with",
|
||||
node_type="event_loop",
|
||||
input_keys=["twitter_handle"],
|
||||
output_keys=["profile_summary"],
|
||||
system_prompt="""\
|
||||
You are a Twitter/X profile researcher. Your job is to thoroughly research a person's public Twitter/X presence.
|
||||
|
||||
Given the Twitter handle provided in your inputs, do the following:
|
||||
|
||||
1. Use web_search to find their Twitter/X profile and any relevant public information about them.
|
||||
2. Use web_scrape to read their Twitter/X profile page (try https://x.com/{handle} or https://twitter.com/{handle}).
|
||||
3. Extract and analyze:
|
||||
- Their bio and self-description
|
||||
- Recent tweets and topics they post about
|
||||
- Professional interests, projects, or accomplishments
|
||||
- Any recurring themes or passions
|
||||
- Specific tweets worth referencing in outreach
|
||||
4. Look for additional context (personal website, blog, other social profiles mentioned in bio).
|
||||
|
||||
Compile a comprehensive profile summary that would help someone write a highly personalized outreach email.
|
||||
|
||||
Use set_output("profile_summary", <your detailed summary as a string>) to store your findings.
|
||||
|
||||
Do NOT return raw JSON. Use the set_output tool to produce outputs.
|
||||
""",
|
||||
tools=["web_search", "web_scrape"],
|
||||
)
|
||||
|
||||
# Node 3: Draft & Review (client-facing)
|
||||
# Drafts a personalized email, presents to user, iterates until approved.
|
||||
draft_review_node = NodeSpec(
|
||||
id="draft-review",
|
||||
name="Draft & Review",
|
||||
description="Draft a personalized outreach email using profile research, present to user for review, and iterate until approved",
|
||||
node_type="event_loop",
|
||||
client_facing=True,
|
||||
input_keys=["outreach_context", "recipient_email", "profile_summary"],
|
||||
output_keys=["approved_email"],
|
||||
system_prompt="""\
|
||||
You are an expert email copywriter specializing in personalized outreach.
|
||||
|
||||
You have been given:
|
||||
- A profile summary of the target person (from their Twitter/X)
|
||||
- The outreach context/purpose
|
||||
- The recipient's email address
|
||||
|
||||
**STEP 1 — Draft and present the email (text only, NO tool calls):**
|
||||
|
||||
Using the profile research, draft a personalized outreach email that:
|
||||
- References at least 2 specific details from their Twitter profile (tweets, interests, projects)
|
||||
- Clearly connects to the outreach purpose
|
||||
- Includes a specific, relevant call to action
|
||||
- Is professional but conversational and authentic — NOT spammy, robotic, or overly formal
|
||||
- Is concise (under 300 words)
|
||||
|
||||
Present the complete email draft to the user, formatted clearly with Subject line and Body.
|
||||
Then ask: "Would you like any changes, or shall I send this?"
|
||||
|
||||
If the user requests changes, revise the email and present the updated version. Keep iterating until the user is satisfied.
|
||||
|
||||
**STEP 2 — After the user explicitly approves the email, call set_output:**
|
||||
- set_output("approved_email", "<the final approved email text including subject line>")
|
||||
""",
|
||||
tools=[],
|
||||
)
|
||||
|
||||
# Node 4: Send
|
||||
# Sends the approved email using the send_email tool.
|
||||
send_node = NodeSpec(
|
||||
id="send",
|
||||
name="Send",
|
||||
description="Send the approved outreach email to the recipient",
|
||||
node_type="event_loop",
|
||||
input_keys=["approved_email", "recipient_email"],
|
||||
output_keys=["delivery_status"],
|
||||
system_prompt="""\
|
||||
You are responsible for sending the approved outreach email.
|
||||
|
||||
You have the approved email text and the recipient's email address in your inputs.
|
||||
|
||||
Parse the subject line and body from the approved_email, then use the send_email tool to send it to the recipient_email address.
|
||||
|
||||
After sending successfully, call:
|
||||
- set_output("delivery_status", "sent")
|
||||
|
||||
If there is an error sending, call:
|
||||
- set_output("delivery_status", "failed: <error details>")
|
||||
|
||||
Do NOT return raw JSON. Use the set_output tool to produce outputs.
|
||||
""",
|
||||
tools=["send_email"],
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"intake_node",
|
||||
"research_node",
|
||||
"draft_review_node",
|
||||
"send_node",
|
||||
]
|
||||
+3
-4
@@ -66,7 +66,7 @@ prompt_choice() {
|
||||
|
||||
local choice
|
||||
while true; do
|
||||
read -r -p "Enter choice (1-${#options[@]}): " choice
|
||||
read -r -p "Enter choice (1-${#options[@]}): " choice || true
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "${#options[@]}" ]; then
|
||||
PROMPT_CHOICE=$((choice - 1))
|
||||
return 0
|
||||
@@ -602,8 +602,7 @@ prompt_model_selection() {
|
||||
|
||||
local choice
|
||||
while true; do
|
||||
read -r -p "Enter choice [1]: " choice
|
||||
choice="${choice:-1}"
|
||||
read -r -p "Enter choice (1-$count): " choice || true
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "$count" ]; then
|
||||
local idx=$((choice - 1))
|
||||
SELECTED_MODEL="$(get_model_choice_id "$provider_id" "$idx")"
|
||||
@@ -721,7 +720,7 @@ if [ ${#FOUND_PROVIDERS[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
|
||||
while true; do
|
||||
read -r -p "Enter choice (1-$max_choice): " choice
|
||||
read -r -p "Enter choice (1-$max_choice): " choice || true
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "$max_choice" ]; then
|
||||
if [ "$choice" -eq "$max_choice" ]; then
|
||||
# Fall through to the manual provider selection below
|
||||
|
||||
Reference in New Issue
Block a user