-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
522 lines (445 loc) · 20.8 KB
/
app.py
File metadata and controls
522 lines (445 loc) · 20.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
import os
import sys
import streamlit as st
from dotenv import load_dotenv
load_dotenv()
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src"))
from knowledge.product_loader import COMPANY_CONFIG, PRODUCTS_CONFIG
from agents.scorer import ScorerAgent
from context import ContextManager
from orchestrator import Orchestrator
from tools.contact_finder import find_prospects
from tools.email_sender import is_configured as gmail_configured
from tools.email_sender import send_outreach_email
# ── Page Config ──
st.set_page_config(
page_title="Agentic Pipeline",
page_icon="AP",
layout="wide",
)
# ── Custom CSS ──
st.markdown(
"""
<style>
.agent-card {
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
}
.stTabs [data-baseweb="tab-list"] {
gap: 8px;
}
</style>
""",
unsafe_allow_html=True,
)
# ── Header ──
st.title("Agentic Pipeline")
st.caption(
"AI-powered prospect research, proposal generation & outreach | "
"Orchestrator + Sub-agents | Structured I/O"
)
# ── Load product data from YAML config (dynamic) ──
products = PRODUCTS_CONFIG
product_list = list(products["products"].values())
case_studies = products["case_studies"]
# ── Sidebar ──
with st.sidebar:
st.header("Products in Scope")
for product in product_list:
with st.expander(f"{product['name']} — {product.get('category', '')}"):
st.write(product["description"])
st.write("**Best for:**")
for item in product["best_for"]:
st.write(f"- {item}")
with st.expander("Case Studies"):
for cs in case_studies:
st.write(f"**{cs['title']}**")
st.write(f"→ {cs['result']}")
st.write("---")
st.header("Settings")
model = st.selectbox(
"LLM Model", ["claude-sonnet-4-20250514", "claude-haiku-4-5-20251001"], index=0
)
st.header("Pipeline Info")
st.caption(f"Company: {COMPANY_CONFIG['name']}")
st.caption(f"Config: config/company.yaml")
# ── Helpers ──
def _format_prospect(prospect: dict) -> str:
"""Format a prospect dict into a text block for agents."""
email_str = ", ".join(prospect["emails"]) if prospect.get("emails") else "Not found"
phone_str = ", ".join(prospect["phones"]) if prospect.get("phones") else "Not found"
return (
f"Company: {prospect['title']}\n"
f"Website: {prospect['url']}\n"
f"Description: {prospect['snippet']}\n"
f"Email(s): {email_str}\n"
f"Phone(s): {phone_str}"
)
# ── Main Content: Two Tabs ──
EXAMPLES = {
"Custom input": "",
"Auto Parts Stamping (Germany)": (
"Mueller Automotive GmbH, Germany. Mid-size automotive parts manufacturer "
"specializing in metal stamping for car body panels and structural components. "
"Operates 3 factories with approximately 150 press machines ranging from 200 to 2000 tons. "
"Supplies to BMW and Volkswagen. Facing EU carbon reporting regulations and rising energy costs."
),
"Copper Fittings (USA)": (
"Pacific Brass & Copper, California, USA. Manufacturer of copper pipe fittings "
"and brass valves for plumbing industry. Single factory with 40+ forging and forming machines. "
"High energy costs due to California electricity rates. No current smart factory systems."
),
"Electronics Stamping (Vietnam)": (
"Vina Precision Parts Co., Ltd, Ho Chi Minh City, Vietnam. Precision metal stamping "
"for consumer electronics — connector pins, shielding cases, battery contacts. "
"80 high-speed stamping presses. Struggling with defect rates and no centralized monitoring. "
"Japanese parent company requires detailed production reporting."
),
}
tab1, tab2 = st.tabs(["Single Proposal", "Prospect Search"])
# ════════════════════════════════════════════════
# TAB 1: Single Proposal (orchestrator-based)
# ════════════════════════════════════════════════
with tab1:
st.header("Target Company")
selected = st.selectbox(
"Choose an example or enter custom:", list(EXAMPLES.keys()), key="sp_example"
)
if selected == "Custom input":
company_input = st.text_area(
"Describe the target company",
height=150,
placeholder=(
"Company name, location, what they manufacture, factory size, "
"number of machines, known pain points..."
),
key="sp_input_custom",
)
else:
company_input = st.text_area(
"Edit or use as-is:", value=EXAMPLES[selected], height=150, key="sp_input_example"
)
run_btn = st.button("Run Pipeline", type="primary", use_container_width=True)
if run_btn and company_input.strip():
company_name = company_input.split(",")[0].split("\n")[0].strip()
st.session_state["sp_company_name"] = company_name
orchestrator = Orchestrator(interactive=False)
# Show plan
plan = orchestrator.plan(company_input)
with st.expander("Execution Plan", expanded=True):
for step in plan.steps:
deps = f" (after: {', '.join(d.value for d in step.depends_on)})" if step.depends_on else ""
st.write(f"**{step.agent.value.title()}** — {step.description}{deps}")
# Execute pipeline with real-time streaming progress
with st.status("Running pipeline...", expanded=True) as status:
def on_event(event, _status=status):
event_type = event.get("type")
agent = event.get("agent", "")
try:
if event_type == "agent_start":
_status.update(label=f"Agent: {agent.title()} — running...")
elif event_type == "agent_end":
duration = event.get("duration", 0)
if event.get("success"):
_status.update(label=f"{agent.title()} done ({duration:.1f}s) — next...")
elif event_type == "tool_call":
tool = event.get("tool", "")
_status.update(label=f"Agent: {agent.title()} — calling {tool}...")
except Exception:
pass # Swallow thread-safety issues from parallel agents
result = orchestrator.execute(company_input, plan, on_event=on_event)
status.update(label="Pipeline complete!", state="complete")
st.session_state["sp_result"] = result
st.session_state["sp_pipeline_complete"] = True
# Save results
paths = orchestrator.save_results(result)
st.session_state["sp_paths"] = paths
elif run_btn:
st.warning("Please enter a company description first.")
# ── Display results ──
if st.session_state.get("sp_pipeline_complete"):
result = st.session_state["sp_result"]
company_name = st.session_state["sp_company_name"]
# Token usage summary
col_t1, col_t2, col_t3 = st.columns(3)
with col_t1:
st.metric("Tokens In", f"{result.total_tokens_in:,}")
with col_t2:
st.metric("Tokens Out", f"{result.total_tokens_out:,}")
with col_t3:
st.metric("Duration", f"{result.total_duration_seconds:.1f}s")
st.success("Pipeline complete!")
# Agent outputs
with st.expander("Research Brief", expanded=False):
if result.research_brief:
st.markdown(result.research_brief.raw_brief)
with st.expander("Competitive Analysis", expanded=False):
if result.competitive_analysis:
st.markdown(result.competitive_analysis.raw_analysis)
with st.expander("Solution Mapping", expanded=False):
if result.solution_map:
st.markdown(result.solution_map.raw_solution_map)
with st.expander("Deal Estimate", expanded=False):
if result.deal_estimate:
st.json(result.deal_estimate.model_dump())
# Proposal
st.header("Generated Proposal")
if result.proposal:
st.markdown(result.proposal.proposal_markdown)
col_dl, col_email = st.columns(2)
with col_dl:
st.download_button(
label="Download Proposal (Markdown)",
data=result.proposal.proposal_markdown,
file_name=f"proposal_{company_name.replace(' ', '_')}.md",
mime="text/markdown",
use_container_width=True,
)
# Email section
if result.proposal.email_subject or result.proposal.email_body:
st.subheader("Cold Email")
email_text = f"Subject: {result.proposal.email_subject}\n\n{result.proposal.email_body}"
edited_email = st.text_area(
"Edit email before sending:",
value=email_text,
height=300,
key="sp_email_editor",
)
recipient = st.text_input("Recipient email:", key="sp_recipient")
col_send, col_dl_email = st.columns(2)
with col_send:
if st.button("Send Email", use_container_width=True, key="sp_send"):
if not recipient or "@" not in recipient:
st.error("Please enter a valid email address.")
elif not gmail_configured():
st.error(
"Gmail not configured. Set GMAIL_ADDRESS and GMAIL_APP_PASSWORD in .env"
)
else:
send_result = send_outreach_email(recipient, edited_email)
if send_result["success"]:
st.success(f"Email sent to {recipient}!")
else:
st.error(f"Send failed: {send_result['error']}")
with col_dl_email:
st.download_button(
label="Download Email (.txt)",
data=edited_email,
file_name=f"email_{company_name.replace(' ', '_')}.txt",
mime="text/plain",
use_container_width=True,
)
# ── Architecture Diagram ──
with st.expander("How the Pipeline Works"):
st.markdown("""
```
User Input (company description)
│
▼
┌────────────────────────────────┐
│ ORCHESTRATOR │
│ 1. Plan execution steps │
│ 2. Dispatch sub-agents │
│ 3. Aggregate results │
└────────────┬───────────────────┘
│
┌─────────┼─────────┐
▼ ▼ ▼
Researcher Analyst Architect (parallel)
│ │ │
└─────────┼─────────┘
▼
Scorer
│
▼
Writer
│
▼
Proposal + Email
```
""")
# ════════════════════════════════════════════════
# TAB 2: Prospect Search
# ════════════════════════════════════════════════
with tab2:
st.header("Find Prospects")
ps_query = st.text_input(
"Search for companies (e.g. 'metal stamping companies Germany'):",
key="ps_query",
)
ps_max = st.slider("Max results", min_value=5, max_value=20, value=10, key="ps_max")
search_btn = st.button("Search", type="primary", use_container_width=True, key="ps_search_btn")
# ── Phase 2: Search + Quick Qualify ──
if search_btn and ps_query.strip():
with st.status("Searching for companies...", expanded=True) as s:
prospects = find_prospects(ps_query, max_results=ps_max, search_delay=0.5)
s.update(label=f"Found {len(prospects)} companies", state="complete")
if not prospects:
st.warning("No companies found. Try a different search query.")
else:
deals = []
progress = st.progress(0, text="Qualifying prospects...")
cm = ContextManager()
scorer = ScorerAgent(cm)
for i, p in enumerate(prospects):
brief = _format_prospect(p)
context = cm.build_context_packet(
task_description=f"Estimate the deal size for this prospect:\n\n{brief}",
relevant_data={},
company_config=COMPANY_CONFIG,
)
result = scorer.run(context)
deals.append(result.output if result.output else {})
progress.progress(
(i + 1) / len(prospects),
text=f"Qualified {i + 1}/{len(prospects)}: {p['title'][:40]}",
)
progress.empty()
st.session_state["ps_prospects"] = prospects
st.session_state["ps_deals"] = deals
st.session_state["ps_search_done"] = True
st.session_state.pop("ps_proposals", None)
st.session_state.pop("ps_batch_done", None)
# ── Phase 3: Results Table + Selection ──
if st.session_state.get("ps_search_done"):
prospects = st.session_state["ps_prospects"]
deals = st.session_state["ps_deals"]
st.subheader("Prospect Pipeline")
import pandas as pd
rows = []
for i, (p, d) in enumerate(zip(prospects, deals)):
email = p["emails"][0] if p["emails"] else "—"
rows.append({
"#": i + 1,
"Company": p["title"][:35],
"Industry": d.get("industry", "—")[:20],
"Email": email,
"Est. Deal": f"${d.get('first_year_value', 0):,.0f}",
"Category": d.get("deal_category", "—"),
"Confidence": d.get("confidence", "—"),
})
df = pd.DataFrame(rows)
st.dataframe(df, use_container_width=True, hide_index=True)
# Selection
options = [f"{i + 1}. {p['title'][:40]}" for i, p in enumerate(prospects)]
selected_labels = st.multiselect(
"Select companies for detailed proposals:",
options,
key="ps_selection",
)
generate_btn = st.button(
"Generate Detailed Proposals",
type="primary",
use_container_width=True,
disabled=len(selected_labels) == 0,
key="ps_generate_btn",
)
# ── Phase 4: Batch Proposal Generation ──
if generate_btn and selected_labels:
selected_indices = [int(label.split(".")[0]) - 1 for label in selected_labels]
proposal_results = {}
for idx in selected_indices:
p = prospects[idx]
d = deals[idx]
company_name = p["title"].split(" - ")[0].split(" | ")[0].strip()
with st.status(
f"Generating proposal for {company_name[:30]}...", expanded=True
) as s:
def on_event(event, _status=s, _name=company_name):
event_type = event.get("type")
agent = event.get("agent", "")
try:
if event_type == "agent_start":
_status.update(label=f"{_name[:20]} — {agent.title()}...")
elif event_type == "tool_call":
tool = event.get("tool", "")
_status.update(label=f"{_name[:20]} — {agent.title()}: {tool}...")
except Exception:
pass
brief = _format_prospect(p)
orchestrator = Orchestrator(interactive=False)
pipeline_result = orchestrator.execute(brief, on_event=on_event)
s.update(label=f"Done: {company_name[:30]}", state="complete")
proposal_results[idx] = {
"company_name": company_name,
"pipeline_result": pipeline_result,
"prospect": p,
"deal": d,
}
st.session_state["ps_proposals"] = proposal_results
st.session_state["ps_batch_done"] = True
# ── Phase 5: Display + Actions ──
if st.session_state.get("ps_batch_done"):
proposal_results = st.session_state["ps_proposals"]
st.subheader("Generated Proposals")
for idx, data in proposal_results.items():
name = data["company_name"]
pr = data["pipeline_result"]
with st.expander(f"{name}", expanded=False):
ptab1, ptab2, ptab3 = st.tabs(["Proposal", "Email", "Research"])
with ptab1:
if pr.proposal:
st.markdown(pr.proposal.proposal_markdown)
st.download_button(
label="Download Proposal",
data=pr.proposal.proposal_markdown,
file_name=f"proposal_{name.replace(' ', '_')}.md",
mime="text/markdown",
use_container_width=True,
key=f"ps_dl_prop_{idx}",
)
with ptab2:
email_text = ""
if pr.proposal and pr.proposal.email_subject:
email_text = f"Subject: {pr.proposal.email_subject}\n\n{pr.proposal.email_body}"
edited = st.text_area(
"Edit email:",
value=email_text,
height=250,
key=f"ps_email_edit_{idx}",
)
p = data["prospect"]
default_email = p["emails"][0] if p["emails"] else ""
recipient = st.text_input(
"Recipient:",
value=default_email,
key=f"ps_recipient_{idx}",
)
col_s, col_d = st.columns(2)
with col_s:
if st.button("Send Email", use_container_width=True, key=f"ps_send_{idx}"):
if not recipient or "@" not in recipient:
st.error("Please enter a valid email address.")
elif not gmail_configured():
st.error(
"Gmail not configured. "
"Set GMAIL_ADDRESS and GMAIL_APP_PASSWORD in .env"
)
else:
result = send_outreach_email(recipient, edited)
if result["success"]:
st.success(f"Sent to {recipient}!")
else:
st.error(f"Failed: {result['error']}")
with col_d:
st.download_button(
label="Download Email",
data=edited,
file_name=f"email_{name.replace(' ', '_')}.txt",
mime="text/plain",
use_container_width=True,
key=f"ps_dl_email_{idx}",
)
with ptab3:
if pr.research_brief:
st.markdown("**Research Brief**")
st.markdown(pr.research_brief.raw_brief)
if pr.solution_map:
st.markdown("---")
st.markdown("**Solution Map**")
st.markdown(pr.solution_map.raw_solution_map)
if pr.competitive_analysis:
st.markdown("---")
st.markdown("**Competitive Analysis**")
st.markdown(pr.competitive_analysis.raw_analysis)