Connor Adams commited on
Commit
ef01a34
Β·
1 Parent(s): b528a04

Add logfire and duckduckgo

Browse files
Files changed (4) hide show
  1. agent.py +15 -4
  2. requirements.lock +55 -2
  3. requirements.txt +2 -0
  4. tools/safe_duck.py +114 -0
agent.py CHANGED
@@ -1,9 +1,20 @@
 
 
 
 
 
 
 
1
  class BasicAgent:
2
  def __init__(self):
3
- print("BasicAgent initialized.")
 
 
 
 
4
 
5
  def __call__(self, question: str) -> str:
6
  print(f"Agent received question (first 50 chars): {question[:50]}...")
7
- fixed_answer = "This is a default answer."
8
- print(f"Agent returning fixed answer: {fixed_answer}")
9
- return fixed_answer
 
1
+ import logfire
2
+
3
+ from pydantic_ai import Agent
4
+ from tools.safe_duck import safe_duckduckgo_search_tool
5
+ logfire.configure()
6
+ logfire.instrument_pydantic_ai()
7
+
8
  class BasicAgent:
9
  def __init__(self):
10
+ self.agent = Agent(
11
+ "openai:o3-mini",
12
+ tools=[safe_duckduckgo_search_tool()],
13
+ system_prompt="Search DuckDuckGo for the given query and return the results.",
14
+ )
15
 
16
  def __call__(self, question: str) -> str:
17
  print(f"Agent received question (first 50 chars): {question[:50]}...")
18
+ result = self.agent.run_sync(question).output
19
+ print(f"Agent returning fixed answer: {result}")
20
+ return result
requirements.lock CHANGED
@@ -41,6 +41,7 @@ charset-normalizer==3.4.2
41
  # via requests
42
  click==8.1.8
43
  # via
 
44
  # typer
45
  # uvicorn
46
  cohere==5.15.0
@@ -50,12 +51,17 @@ colorama==0.4.6
50
  cryptography==45.0.2
51
  # via authlib
52
  deprecated==1.2.18
53
- # via opentelemetry-api
 
 
 
54
  distro==1.9.0
55
  # via
56
  # anthropic
57
  # groq
58
  # openai
 
 
59
  eval-type-backport==0.2.2
60
  # via
61
  # mistralai
@@ -65,6 +71,8 @@ exceptiongroup==1.3.0
65
  # via
66
  # anyio
67
  # pydantic-ai-slim
 
 
68
  fasta2a==0.2.7
69
  # via pydantic-ai-slim
70
  fastapi==0.115.12
@@ -85,6 +93,8 @@ google-auth==2.40.2
85
  # pydantic-ai-slim
86
  google-genai==1.16.1
87
  # via pydantic-ai-slim
 
 
88
  gradio==5.31.0
89
  # via -r requirements.txt
90
  gradio-client==1.10.1
@@ -145,10 +155,14 @@ jmespath==1.0.1
145
  # via
146
  # boto3
147
  # botocore
 
 
148
  logfire-api==3.16.0
149
  # via
150
  # pydantic-evals
151
  # pydantic-graph
 
 
152
  markdown-it-py==3.0.0
153
  # via rich
154
  markupsafe==3.0.2
@@ -170,7 +184,29 @@ openai==1.82.0
170
  opentelemetry-api==1.33.1
171
  # via
172
  # fasta2a
 
 
 
 
173
  # pydantic-ai-slim
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  orjson==3.10.18
175
  # via gradio
176
  packaging==25.0
@@ -178,12 +214,20 @@ packaging==25.0
178
  # gradio
179
  # gradio-client
180
  # huggingface-hub
 
181
  pandas==2.2.3
182
  # via gradio
183
  pillow==11.2.1
184
  # via gradio
 
 
185
  prompt-toolkit==3.0.51
186
  # via pydantic-ai-slim
 
 
 
 
 
187
  pyasn1==0.6.1
188
  # via
189
  # pyasn1-modules
@@ -212,6 +256,7 @@ pydantic-ai==0.2.7
212
  # via -r requirements.txt
213
  pydantic-ai-slim==0.2.7
214
  # via
 
215
  # pydantic-ai
216
  # pydantic-evals
217
  pydantic-core==2.33.2
@@ -252,9 +297,11 @@ requests==2.32.3
252
  # cohere
253
  # google-genai
254
  # huggingface-hub
 
255
  # pydantic-ai-slim
256
  rich==14.0.0
257
  # via
 
258
  # pydantic-ai-slim
259
  # pydantic-evals
260
  # typer
@@ -289,6 +336,8 @@ starlette==0.46.2
289
  # sse-starlette
290
  tokenizers==0.21.1
291
  # via cohere
 
 
292
  tomlkit==0.13.2
293
  # via gradio
294
  tqdm==4.67.1
@@ -311,7 +360,9 @@ typing-extensions==4.13.2
311
  # gradio-client
312
  # groq
313
  # huggingface-hub
 
314
  # openai
 
315
  # pydantic
316
  # pydantic-core
317
  # rich
@@ -343,6 +394,8 @@ websockets==15.0.1
343
  # google-genai
344
  # gradio-client
345
  wrapt==1.17.2
346
- # via deprecated
 
 
347
  zipp==3.21.0
348
  # via importlib-metadata
 
41
  # via requests
42
  click==8.1.8
43
  # via
44
+ # duckduckgo-search
45
  # typer
46
  # uvicorn
47
  cohere==5.15.0
 
51
  cryptography==45.0.2
52
  # via authlib
53
  deprecated==1.2.18
54
+ # via
55
+ # opentelemetry-api
56
+ # opentelemetry-exporter-otlp-proto-http
57
+ # opentelemetry-semantic-conventions
58
  distro==1.9.0
59
  # via
60
  # anthropic
61
  # groq
62
  # openai
63
+ duckduckgo-search==8.0.2
64
+ # via pydantic-ai-slim
65
  eval-type-backport==0.2.2
66
  # via
67
  # mistralai
 
71
  # via
72
  # anyio
73
  # pydantic-ai-slim
74
+ executing==2.2.0
75
+ # via logfire
76
  fasta2a==0.2.7
77
  # via pydantic-ai-slim
78
  fastapi==0.115.12
 
93
  # pydantic-ai-slim
94
  google-genai==1.16.1
95
  # via pydantic-ai-slim
96
+ googleapis-common-protos==1.70.0
97
+ # via opentelemetry-exporter-otlp-proto-http
98
  gradio==5.31.0
99
  # via -r requirements.txt
100
  gradio-client==1.10.1
 
155
  # via
156
  # boto3
157
  # botocore
158
+ logfire==3.16.0
159
+ # via pydantic-ai
160
  logfire-api==3.16.0
161
  # via
162
  # pydantic-evals
163
  # pydantic-graph
164
+ lxml==5.4.0
165
+ # via duckduckgo-search
166
  markdown-it-py==3.0.0
167
  # via rich
168
  markupsafe==3.0.2
 
184
  opentelemetry-api==1.33.1
185
  # via
186
  # fasta2a
187
+ # opentelemetry-exporter-otlp-proto-http
188
+ # opentelemetry-instrumentation
189
+ # opentelemetry-sdk
190
+ # opentelemetry-semantic-conventions
191
  # pydantic-ai-slim
192
+ opentelemetry-exporter-otlp-proto-common==1.33.1
193
+ # via opentelemetry-exporter-otlp-proto-http
194
+ opentelemetry-exporter-otlp-proto-http==1.33.1
195
+ # via logfire
196
+ opentelemetry-instrumentation==0.54b1
197
+ # via logfire
198
+ opentelemetry-proto==1.33.1
199
+ # via
200
+ # opentelemetry-exporter-otlp-proto-common
201
+ # opentelemetry-exporter-otlp-proto-http
202
+ opentelemetry-sdk==1.33.1
203
+ # via
204
+ # logfire
205
+ # opentelemetry-exporter-otlp-proto-http
206
+ opentelemetry-semantic-conventions==0.54b1
207
+ # via
208
+ # opentelemetry-instrumentation
209
+ # opentelemetry-sdk
210
  orjson==3.10.18
211
  # via gradio
212
  packaging==25.0
 
214
  # gradio
215
  # gradio-client
216
  # huggingface-hub
217
+ # opentelemetry-instrumentation
218
  pandas==2.2.3
219
  # via gradio
220
  pillow==11.2.1
221
  # via gradio
222
+ primp==0.15.0
223
+ # via duckduckgo-search
224
  prompt-toolkit==3.0.51
225
  # via pydantic-ai-slim
226
+ protobuf==5.29.4
227
+ # via
228
+ # googleapis-common-protos
229
+ # logfire
230
+ # opentelemetry-proto
231
  pyasn1==0.6.1
232
  # via
233
  # pyasn1-modules
 
256
  # via -r requirements.txt
257
  pydantic-ai-slim==0.2.7
258
  # via
259
+ # -r requirements.txt
260
  # pydantic-ai
261
  # pydantic-evals
262
  pydantic-core==2.33.2
 
297
  # cohere
298
  # google-genai
299
  # huggingface-hub
300
+ # opentelemetry-exporter-otlp-proto-http
301
  # pydantic-ai-slim
302
  rich==14.0.0
303
  # via
304
+ # logfire
305
  # pydantic-ai-slim
306
  # pydantic-evals
307
  # typer
 
336
  # sse-starlette
337
  tokenizers==0.21.1
338
  # via cohere
339
+ tomli==2.2.1
340
+ # via logfire
341
  tomlkit==0.13.2
342
  # via gradio
343
  tqdm==4.67.1
 
360
  # gradio-client
361
  # groq
362
  # huggingface-hub
363
+ # logfire
364
  # openai
365
+ # opentelemetry-sdk
366
  # pydantic
367
  # pydantic-core
368
  # rich
 
394
  # google-genai
395
  # gradio-client
396
  wrapt==1.17.2
397
+ # via
398
+ # deprecated
399
+ # opentelemetry-instrumentation
400
  zipp==3.21.0
401
  # via importlib-metadata
requirements.txt CHANGED
@@ -2,3 +2,5 @@ gradio
2
  gradio[oauth]
3
  requests
4
  pydantic-ai
 
 
 
2
  gradio[oauth]
3
  requests
4
  pydantic-ai
5
+ pydantic-ai-slim[duckduckgo]
6
+ pydantic-ai[logfire]
tools/safe_duck.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ safe_duck_tool.py
3
+ A resilient, family-friendly DuckDuckGo search Tool for Pydantic-AI.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import functools
9
+ import time
10
+ from dataclasses import dataclass
11
+ from typing_extensions import TypedDict
12
+
13
+ import anyio
14
+ import anyio.to_thread
15
+ from duckduckgo_search import DDGS, exceptions
16
+ from pydantic import TypeAdapter
17
+ from pydantic_ai.tools import Tool
18
+
19
+
20
+ # ──────────────────────────────────────────────────────────────────────────
21
+ # 1. Types
22
+ # ──────────────────────────────────────────────────────────────────────────
23
+ class DuckDuckGoResult(TypedDict):
24
+ title: str
25
+ href: str
26
+ body: str
27
+
28
+
29
+ duckduckgo_ta = TypeAdapter(list[DuckDuckGoResult])
30
+
31
+
32
+ # ──────────────────────────────────────────────────────────────────────────
33
+ # 2. Search wrapper with cache + back-off
34
+ # ──────────────────────────────────────────────────────────────────────────
35
+ @functools.lru_cache(maxsize=512)
36
+ def _safe_search(
37
+ query: str,
38
+ *,
39
+ ddgs_constructor_kwargs_tuple: tuple,
40
+ safesearch: str,
41
+ max_results: int | None,
42
+ retries: int = 5,
43
+ ) -> list[dict[str, str]]:
44
+ wait = 1
45
+ for _ in range(retries):
46
+ try:
47
+
48
+ ddgs = DDGS(**dict(ddgs_constructor_kwargs_tuple))
49
+
50
+ return list(
51
+ ddgs.text(query, safesearch=safesearch, max_results=max_results)
52
+ )
53
+ except exceptions.RatelimitException as e:
54
+ time.sleep(getattr(e, "retry_after", wait))
55
+ wait = min(wait * 2, 30)
56
+ raise RuntimeError("DuckDuckGo kept rate-limiting after multiple attempts")
57
+
58
+
59
+ # ──────────────────────────────────────────────────────────────────────────
60
+ # 3. Tool implementation
61
+ # ──────────────────────────────────────────────────────────────────────────
62
+ @dataclass
63
+ class _SafeDuckToolImpl:
64
+ ddgs_constructor_kwargs: dict # Renamed from client_kwargs
65
+ safesearch: str # Added to store safesearch setting
66
+ max_results: int | None
67
+
68
+ async def __call__(self, query: str) -> list[DuckDuckGoResult]:
69
+ search = functools.partial(
70
+ _safe_search,
71
+ # Convert dict to sorted tuple of items to make it hashable
72
+ ddgs_constructor_kwargs_tuple=tuple(
73
+ sorted(self.ddgs_constructor_kwargs.items())
74
+ ),
75
+ safesearch=self.safesearch, # Pass stored safesearch
76
+ max_results=self.max_results,
77
+ )
78
+ results = await anyio.to_thread.run_sync(search, query)
79
+ # validate & coerce with Pydantic
80
+ return duckduckgo_ta.validate_python(results)
81
+
82
+
83
+ def safe_duckduckgo_search_tool(
84
+ *,
85
+ safesearch: str = "moderate", # "on" | "moderate" | "off"
86
+ timeout: int = 15,
87
+ max_results: int | None = None,
88
+ proxy: str | None = None, # e.g. "socks5h://user:pw@host:1080"
89
+ ) -> Tool:
90
+ """
91
+ Create a resilient, Safe-Search-enabled DuckDuckGo search Tool.
92
+
93
+ Drop-in replacement for `pydantic_ai.common_tools.duckduckgo.duckduckgo_search_tool`.
94
+ """
95
+ # Arguments for DDGS constructor
96
+ ddgs_constructor_kwargs = dict(
97
+ timeout=timeout,
98
+ proxy=proxy,
99
+ )
100
+ # Arguments for ddgs.text() method are handled separately (safesearch, max_results)
101
+
102
+ impl = _SafeDuckToolImpl(
103
+ ddgs_constructor_kwargs=ddgs_constructor_kwargs,
104
+ safesearch=safesearch,
105
+ max_results=max_results,
106
+ )
107
+ return Tool(
108
+ impl.__call__,
109
+ name="safe_duckduckgo_search",
110
+ description=(
111
+ "DuckDuckGo web search with Safe Search, automatic back-off, and "
112
+ "LRU caching. Pass a plain-text query; returns a list of results."
113
+ ),
114
+ )