Skip to content

Commit 83eec1f

Browse files
authored
Merge pull request #1 from aseifert/codex/create-sales_lead_researcher-example
Add sales_lead_researcher example
2 parents 6e078bf + 5be4cd6 commit 83eec1f

File tree

10 files changed

+200
-2
lines changed

10 files changed

+200
-2
lines changed

docs/examples.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Check out a variety of sample implementations of the SDK in the examples section
3737

3838
- **customer_service**: Example customer service system for an airline.
3939
- **research_bot**: Simple deep research clone.
40+
- **sales_lead_researcher**: Collect CRM information about publishing contacts.
4041

4142
- **[voice](https://github.com/openai/openai-agents-python/tree/main/examples/voice):**
4243
See examples of voice agents, using our TTS and STT models.

docs/ja/examples.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ search:
3838
- **[customer_service](https://github.com/openai/openai-agents-python/tree/main/examples/customer_service)****[research_bot](https://github.com/openai/openai-agents-python/tree/main/examples/research_bot):**
3939
より実践的なユースケースを示す、拡張された 2 つの例です。
4040

41-
- **customer_service**: 航空会社向けカスタマーサービスシステムの例
42-
- **research_bot**: シンプルなディープリサーチクローン
41+
- **customer_service**: 航空会社向けカスタマーサービスシステムの例
42+
- **research_bot**: シンプルなディープリサーチクローン
43+
- **sales_lead_researcher**: 出版社向けリード情報を収集する例
4344

4445
- **[voice](https://github.com/openai/openai-agents-python/tree/main/examples/voice):**
4546
TTS と STT モデルを用いた音声エージェントの例をご覧ください。
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Sales lead researcher
2+
3+
This example shows how to build a small multi‑agent workflow for gathering sales leads. The flow mirrors the `research_bot` example but is specialized for collecting CRM information about publishing contacts.
4+
5+
Run it with:
6+
7+
```bash
8+
python -m examples.sales_lead_researcher.main
9+
```
10+
11+
You will be prompted to enter company and person pairs. Provide either `Company, Person` or just the company name. The agents will search the web (LinkedIn and company websites) in parallel and return structured lead information.

examples/sales_lead_researcher/__init__.py

Whitespace-only changes.

examples/sales_lead_researcher/agents/__init__.py

Whitespace-only changes.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from pydantic import BaseModel
2+
3+
from agents import Agent, WebSearchTool
4+
from agents.model_settings import ModelSettings
5+
6+
INSTRUCTIONS = (
7+
"You find the best editorial contact at a publishing company. Given just the company's name, "
8+
"search the web to identify a likely decision maker we could reach out to. "
9+
"Prefer people responsible for content or editorial decisions. "
10+
"Return their full name and title. If unsure, make your best guess based on available information."
11+
)
12+
13+
14+
class ContactInfo(BaseModel):
15+
full_name: str
16+
title: str
17+
18+
19+
contact_finder_agent = Agent(
20+
name="ContactFinderAgent",
21+
instructions=INSTRUCTIONS,
22+
tools=[WebSearchTool()],
23+
model_settings=ModelSettings(tool_choice="required"),
24+
output_type=ContactInfo,
25+
)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from pydantic import BaseModel
2+
3+
from agents import Agent, WebSearchTool
4+
from agents.model_settings import ModelSettings
5+
6+
INSTRUCTIONS = (
7+
"You gather sales lead information for our CRM. "
8+
"Given a person's name and company, search the web (LinkedIn and the company's website) "
9+
"and fill out the following fields as best you can. Use concise language."
10+
)
11+
12+
13+
class LeadInfo(BaseModel):
14+
# Basic contact data
15+
full_name: str
16+
title: str
17+
position: str
18+
email: str | None
19+
phone: str | None
20+
linkedin: str | None
21+
22+
# Company data
23+
company: str
24+
company_type: str | None
25+
company_size: str | None
26+
headquarters: str | None
27+
website: str | None
28+
languages: str | None
29+
publication_formats: str | None
30+
31+
# Publication/content data
32+
publication_types: str | None
33+
publication_frequency: str | None
34+
topics: str | None
35+
audience: str | None
36+
37+
38+
info_agent = Agent(
39+
name="LeadInfoAgent",
40+
instructions=INSTRUCTIONS,
41+
tools=[WebSearchTool()],
42+
model_settings=ModelSettings(tool_choice="required"),
43+
output_type=LeadInfo,
44+
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import asyncio
2+
3+
from .manager import SalesLeadResearcher
4+
5+
6+
async def main() -> None:
7+
print("Enter targets as 'Company, Person' or just 'Company'. Enter an empty line to finish.")
8+
targets: list[tuple[str, str | None]] = []
9+
while True:
10+
line = input()
11+
if not line.strip():
12+
break
13+
if "," in line:
14+
company, person = [part.strip() for part in line.split(",", 1)]
15+
else:
16+
company, person = line.strip(), None
17+
targets.append((company, person))
18+
19+
results = await SalesLeadResearcher().run(targets)
20+
print("\n=====RESULTS=====")
21+
for res in results:
22+
print(res.json())
23+
24+
25+
if __name__ == "__main__":
26+
asyncio.run(main())
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
5+
from rich.console import Console
6+
7+
from agents import Runner, gen_trace_id, trace
8+
9+
from .agents.contact_finder_agent import ContactInfo, contact_finder_agent
10+
from .agents.info_agent import LeadInfo, info_agent
11+
from .printer import Printer
12+
13+
14+
class SalesLeadResearcher:
15+
def __init__(self) -> None:
16+
self.console = Console()
17+
self.printer = Printer(self.console)
18+
19+
async def run(self, targets: list[tuple[str, str | None]]) -> list[LeadInfo]:
20+
trace_id = gen_trace_id()
21+
with trace("Sales lead research", trace_id=trace_id):
22+
self.printer.update_item(
23+
"trace_id",
24+
f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}",
25+
is_done=True,
26+
hide_checkmark=True,
27+
)
28+
tasks = [
29+
asyncio.create_task(self._process(company, person)) for company, person in targets
30+
]
31+
results: list[LeadInfo] = []
32+
num_done = 0
33+
for task in asyncio.as_completed(tasks):
34+
result = await task
35+
results.append(result)
36+
num_done += 1
37+
self.printer.update_item("progress", f"Processed {num_done}/{len(tasks)} leads")
38+
self.printer.mark_item_done("progress")
39+
self.printer.end()
40+
return results
41+
42+
async def _process(self, company: str, person: str | None) -> LeadInfo:
43+
if not person:
44+
result = await Runner.run(contact_finder_agent, company)
45+
info = result.final_output_as(ContactInfo)
46+
person = info.full_name
47+
input_data = f"Person: {person}\nCompany: {company}"
48+
result = await Runner.run(info_agent, input_data)
49+
return result.final_output_as(LeadInfo)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from typing import Any
2+
3+
from rich.console import Console, Group
4+
from rich.live import Live
5+
from rich.spinner import Spinner
6+
7+
8+
class Printer:
9+
def __init__(self, console: Console):
10+
self.live = Live(console=console)
11+
self.items: dict[str, tuple[str, bool]] = {}
12+
self.hide_done_ids: set[str] = set()
13+
self.live.start()
14+
15+
def end(self) -> None:
16+
self.live.stop()
17+
18+
def hide_done_checkmark(self, item_id: str) -> None:
19+
self.hide_done_ids.add(item_id)
20+
21+
def update_item(
22+
self, item_id: str, content: str, is_done: bool = False, hide_checkmark: bool = False
23+
) -> None:
24+
self.items[item_id] = (content, is_done)
25+
if hide_checkmark:
26+
self.hide_done_ids.add(item_id)
27+
self.flush()
28+
29+
def mark_item_done(self, item_id: str) -> None:
30+
self.items[item_id] = (self.items[item_id][0], True)
31+
self.flush()
32+
33+
def flush(self) -> None:
34+
renderables: list[Any] = []
35+
for item_id, (content, is_done) in self.items.items():
36+
if is_done:
37+
prefix = "✅ " if item_id not in self.hide_done_ids else ""
38+
renderables.append(prefix + content)
39+
else:
40+
renderables.append(Spinner("dots", text=content))
41+
self.live.update(Group(*renderables))

0 commit comments

Comments
 (0)