Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit81bd32b

Browse files
authored
Add Slack Lead Qualifier example (#2079)
1 parentf3b9981 commit81bd32b

File tree

15 files changed

+855
-5
lines changed

15 files changed

+855
-5
lines changed

‎docs/api/format_prompt.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#`pydantic_ai.format_prompt`
2+
3+
::: pydantic_ai.format_prompt
4+
options:
5+
members:
6+
- format_as_xml

‎docs/examples/slack-lead-qualifier.md

Lines changed: 265 additions & 0 deletions
Large diffs are not rendered by default.
124 KB
Loading
52.4 KB
Loading

‎examples/pydantic_ai_examples/slack_lead_qualifier/__init__.py

Whitespace-only changes.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
fromtextwrapimportdedent
2+
fromtypesimportNoneType
3+
4+
importlogfire
5+
6+
### [imports]
7+
frompydantic_aiimportAgent,NativeOutput
8+
frompydantic_ai.common_tools.duckduckgoimportduckduckgo_search_tool### [/imports]
9+
10+
from .modelsimportAnalysis,Profile
11+
12+
### [agent]
13+
agent=Agent(
14+
'openai:gpt-4o',
15+
instructions=dedent(
16+
"""
17+
When a new person joins our public Slack, please put together a brief snapshot so we can be most useful to them.
18+
19+
**What to include**
20+
21+
1. **Who they are:** Any details about their professional role or projects (e.g. LinkedIn, GitHub, company bio).
22+
2. **Where they work:** Name of the organisation and its domain.
23+
3. **How we can help:** On a scale of 1–5, estimate how likely they are to benefit from **Pydantic Logfire**
24+
(our paid observability tool) based on factors such as company size, product maturity, or AI usage.
25+
*1 = probably not relevant, 5 = very strong fit.*
26+
27+
**Our products (for context only)**
28+
• **Pydantic Validation** – Python data-validation (open source)
29+
• **Pydantic AI** – Python agent framework (open source)
30+
• **Pydantic Logfire** – Observability for traces, logs & metrics with first-class AI support (commercial)
31+
32+
**How to research**
33+
34+
• Use the provided DuckDuckGo search tool to research the person and the organization they work for, based on the email domain or what you find on e.g. LinkedIn and GitHub.
35+
• If you can't find enough to form a reasonable view, return **None**.
36+
"""
37+
),
38+
tools=[duckduckgo_search_tool()],
39+
output_type=NativeOutput([Analysis,NoneType]),
40+
)### [/agent]
41+
42+
43+
### [analyze_profile]
44+
@logfire.instrument('Analyze profile')
45+
asyncdefanalyze_profile(profile:Profile)->Analysis|None:
46+
result=awaitagent.run(profile.as_prompt())
47+
returnresult.output### [/analyze_profile]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
fromtypingimportAny
2+
3+
importlogfire
4+
fromfastapiimportFastAPI,HTTPException,status
5+
fromlogfire.propagateimportget_context
6+
7+
from .modelsimportProfile
8+
9+
10+
### [process_slack_member]
11+
defprocess_slack_member(profile:Profile):
12+
from .modalimportprocess_slack_memberas_process_slack_member
13+
14+
_process_slack_member.spawn(
15+
profile.model_dump(),logfire_ctx=get_context()
16+
)### [/process_slack_member]
17+
18+
19+
### [app]
20+
app=FastAPI()
21+
logfire.instrument_fastapi(app,capture_headers=True)
22+
23+
24+
@app.post('/')
25+
asyncdefprocess_webhook(payload:dict[str,Any])->dict[str,Any]:
26+
ifpayload['type']=='url_verification':
27+
return {'challenge':payload['challenge']}
28+
elif (
29+
payload['type']=='event_callback'andpayload['event']['type']=='team_join'
30+
):
31+
profile=Profile.model_validate(payload['event']['user']['profile'])
32+
33+
process_slack_member(profile)
34+
return {'status':'OK'}
35+
36+
raiseHTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)### [/app]
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
importlogfire
2+
3+
### [imports]
4+
from .agentimportanalyze_profile
5+
from .modelsimportProfile
6+
7+
### [imports-daily_summary]
8+
from .slackimportsend_slack_message
9+
from .storeimportAnalysisStore### [/imports,/imports-daily_summary]
10+
11+
### [constant-new_lead_channel]
12+
NEW_LEAD_CHANNEL='#new-slack-leads'
13+
### [/constant-new_lead_channel]
14+
### [constant-daily_summary_channel]
15+
DAILY_SUMMARY_CHANNEL='#daily-slack-leads-summary'
16+
### [/constant-daily_summary_channel]
17+
18+
19+
### [process_slack_member]
20+
@logfire.instrument('Process Slack member')
21+
asyncdefprocess_slack_member(profile:Profile):
22+
analysis=awaitanalyze_profile(profile)
23+
logfire.info('Analysis',analysis=analysis)
24+
25+
ifanalysisisNone:
26+
return
27+
28+
awaitAnalysisStore().add(analysis)
29+
30+
awaitsend_slack_message(
31+
NEW_LEAD_CHANNEL,
32+
[
33+
{
34+
'type':'header',
35+
'text': {
36+
'type':'plain_text',
37+
'text':f'New Slack member with score{analysis.relevance}/5',
38+
},
39+
},
40+
{
41+
'type':'divider',
42+
},
43+
*analysis.as_slack_blocks(),
44+
],
45+
)### [/process_slack_member]
46+
47+
48+
### [send_daily_summary]
49+
@logfire.instrument('Send daily summary')
50+
asyncdefsend_daily_summary():
51+
analyses=awaitAnalysisStore().list()
52+
logfire.info('Analyses',analyses=analyses)
53+
54+
iflen(analyses)==0:
55+
return
56+
57+
sorted_analyses=sorted(analyses,key=lambdax:x.relevance,reverse=True)
58+
top_analyses=sorted_analyses[:5]
59+
60+
blocks= [
61+
{
62+
'type':'header',
63+
'text': {
64+
'type':'plain_text',
65+
'text':f'Top{len(top_analyses)} new Slack members from the last 24 hours',
66+
},
67+
},
68+
]
69+
70+
foranalysisintop_analyses:
71+
blocks.extend(
72+
[
73+
{
74+
'type':'divider',
75+
},
76+
*analysis.as_slack_blocks(include_relevance=True),
77+
]
78+
)
79+
80+
awaitsend_slack_message(
81+
DAILY_SUMMARY_CHANNEL,
82+
blocks,
83+
)
84+
85+
awaitAnalysisStore().clear()### [/send_daily_summary]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
fromtypingimportAny
2+
3+
### [setup_modal]
4+
importmodal
5+
6+
image=modal.Image.debian_slim(python_version='3.13').pip_install(
7+
'pydantic',
8+
'pydantic_ai_slim[openai,duckduckgo]',
9+
'logfire[httpx,fastapi]',
10+
'fastapi[standard]',
11+
'httpx',
12+
)
13+
app=modal.App(
14+
name='slack-lead-qualifier',
15+
image=image,
16+
secrets=[
17+
modal.Secret.from_name('logfire'),
18+
modal.Secret.from_name('openai'),
19+
modal.Secret.from_name('slack'),
20+
],
21+
)### [/setup_modal]
22+
23+
24+
### [setup_logfire]
25+
defsetup_logfire():
26+
importlogfire
27+
28+
logfire.configure(service_name=app.name)
29+
logfire.instrument_pydantic_ai()
30+
logfire.instrument_httpx(capture_all=True)### [/setup_logfire]
31+
32+
33+
### [web_app]
34+
@app.function(min_containers=1)
35+
@modal.asgi_app()# type: ignore
36+
defweb_app():
37+
setup_logfire()
38+
39+
from .appimportappas_app
40+
41+
return_app### [/web_app]
42+
43+
44+
### [process_slack_member]
45+
@app.function()
46+
asyncdefprocess_slack_member(profile_raw:dict[str,Any],logfire_ctx:Any):
47+
setup_logfire()
48+
49+
fromlogfire.propagateimportattach_context
50+
51+
from .functionsimportprocess_slack_memberas_process_slack_member
52+
from .modelsimportProfile
53+
54+
withattach_context(logfire_ctx):
55+
profile=Profile.model_validate(profile_raw)
56+
await_process_slack_member(profile)### [/process_slack_member]
57+
58+
59+
### [send_daily_summary]
60+
@app.function(schedule=modal.Cron('0 8 * * *'))# Every day at 8am UTC
61+
asyncdefsend_daily_summary():
62+
setup_logfire()
63+
64+
from .functionsimportsend_daily_summaryas_send_daily_summary
65+
66+
await_send_daily_summary()### [/send_daily_summary]
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
fromtypingimportAnnotated,Any
2+
3+
fromannotated_typesimportGe,Le
4+
frompydanticimportBaseModel
5+
6+
### [import-format_as_xml]
7+
frompydantic_aiimportformat_as_xml### [/import-format_as_xml]
8+
9+
10+
### [profile,profile-intro]
11+
classProfile(BaseModel):### [/profile-intro]
12+
first_name:str|None=None
13+
last_name:str|None=None
14+
display_name:str|None=None
15+
email:str### [/profile]
16+
17+
### [profile-as_prompt]
18+
defas_prompt(self)->str:
19+
returnformat_as_xml(self,root_tag='profile')### [/profile-as_prompt]
20+
21+
22+
### [analysis,analysis-intro]
23+
classAnalysis(BaseModel):### [/analysis-intro]
24+
profile:Profile
25+
organization_name:str
26+
organization_domain:str
27+
job_title:str
28+
relevance:Annotated[int,Ge(1),Le(5)]
29+
"""Estimated fit for Pydantic Logfire: 1 = low, 5 = high"""
30+
summary:str
31+
"""One-sentence welcome note summarising who they are and how we might help"""### [/analysis]
32+
33+
### [analysis-as_slack_blocks]
34+
defas_slack_blocks(self,include_relevance:bool=False)->list[dict[str,Any]]:
35+
profile=self.profile
36+
relevance=f'({self.relevance}/5)'ifinclude_relevanceelse''
37+
return [
38+
{
39+
'type':'markdown',
40+
'text':f'[{profile.display_name}](mailto:{profile.email}),{self.job_title} at [**{self.organization_name}**](https://{self.organization_domain}){relevance}',
41+
},
42+
{
43+
'type':'markdown',
44+
'text':self.summary,
45+
},
46+
]### [/analysis-as_slack_blocks]

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp