Skip to content

Commit 7d4d685

Browse files
authored
Merge pull request #1 from Burhan-Q/reorder-args
v0.2.0 -- Adds Pydantic-AI integration
2 parents 60cbf87 + a5c7fd7 commit 7d4d685

7 files changed

Lines changed: 690 additions & 17 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@ wheels/
1616
MacOS
1717
.DS_Store
1818
Thumbs.db
19+
20+
.*_docs/
21+
.coverage

examples/01-simple_quarterly_review.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def calculate_growth_rate(current: float, previous: float) -> dict[str, str]:
5353
"""Calculate simple growth rate."""
5454
if previous == 0:
5555
return {"growth_rate": "N/A", "trend": "new"}
56-
56+
5757
rate = (current - previous) / previous
5858
trend = "up" if rate > 0 else "down"
5959
return {"growth_rate": f"{rate:.1%}", "trend": trend}
@@ -104,7 +104,7 @@ def formatter(self) -> str:
104104
# Format with proper indentation
105105
data = "\n\n".join(formatted_data)
106106
data = indent(data, INDENT_12)
107-
107+
108108
tools_desc = ", ".join(tool.tool_name for tool in self.tools)
109109
tools_desc = indent(tools_desc, INDENT_12)
110110

@@ -165,7 +165,7 @@ def render(self) -> str:
165165

166166
def main():
167167
"""Demonstrate the simple quarterly review."""
168-
168+
169169
print("🚀 Simple Quarterly Review Example")
170170
print("=" * 35)
171171

@@ -188,4 +188,4 @@ def main():
188188

189189

190190
if __name__ == "__main__":
191-
main()
191+
main()
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Pydantic-AI Tools Integration Example
4+
5+
This example demonstrates how to use pydantic-ai Tool and FunctionToolset objects
6+
with proompt's PromptSection. It shows:
7+
- Creating pydantic-ai Tool objects
8+
- Creating a FunctionToolset
9+
- Mixing ToolContext and pydantic-ai tools
10+
- Using tools in a PromptSection
11+
"""
12+
13+
from textwrap import dedent, indent
14+
15+
from pydantic_ai import FunctionToolset, RunContext, Tool
16+
17+
from proompt.base.context import ToolContext
18+
from proompt.base.prompt import BasePrompt, PromptSection
19+
20+
# ===== DEFINE SOME TOOLS USING PYDANTIC-AI =====
21+
22+
23+
def search_documents(query: str, max_results: int = 5) -> str:
24+
"""Search through company documents."""
25+
return f"Found {max_results} documents matching '{query}'"
26+
27+
28+
def get_company_metrics(metric_type: str) -> dict:
29+
"""Retrieve company metrics."""
30+
return {"revenue": 1000000, "users": 50000, "growth": 0.15}
31+
32+
33+
def calculate_percentage(numerator: float, denominator: float) -> float:
34+
"""Calculate percentage from two numbers."""
35+
if denominator == 0:
36+
return 0.0
37+
return (numerator / denominator) * 100
38+
39+
40+
# Create pydantic-ai Tool objects
41+
search_tool = Tool(search_documents, takes_ctx=False)
42+
metrics_tool = Tool(get_company_metrics, takes_ctx=False)
43+
calc_tool = Tool(calculate_percentage, takes_ctx=False)
44+
45+
# Create a FunctionToolset
46+
analysis_toolset = FunctionToolset(tools=[search_tool, metrics_tool])
47+
48+
49+
# You can also add tools using the decorator
50+
@analysis_toolset.tool
51+
def summarize_data(ctx: RunContext, data: str) -> str:
52+
"""Summarize the provided data."""
53+
return f"Summary of {len(data)} characters of data"
54+
55+
56+
# ===== DEFINE A TRADITIONAL PROOMPT TOOL =====
57+
58+
59+
def format_report(data: dict) -> str:
60+
"""Format data into a readable report."""
61+
return "\n".join(f"{k}: {v}" for k, v in data.items())
62+
63+
64+
proompt_tool = ToolContext(format_report)
65+
66+
67+
# ===== CREATE A PROMPT SECTION WITH MIXED TOOLS =====
68+
69+
70+
class AnalysisSection(PromptSection):
71+
"""A section that uses both proompt and pydantic-ai tools."""
72+
73+
def formatter(self) -> str:
74+
tools_list = "\n" + "\n".join(f"- {tool.tool_name}: {tool.tool_description}" for tool in self.tools)
75+
return dedent(f"""\
76+
## ANALYSIS TOOLS
77+
78+
You have access to the following tools:
79+
{indent(tools_list, " " * 12)}
80+
81+
Use these tools to gather and analyze data for your report.
82+
""").strip()
83+
84+
def render(self) -> str:
85+
return self.formatter()
86+
87+
88+
# ===== CREATE THE PROMPT =====
89+
90+
91+
class MixedToolsPrompt(BasePrompt):
92+
"""Example prompt using both proompt and pydantic-ai tools."""
93+
94+
def render(self) -> str:
95+
return "\n\n".join(section.render() for section in self.sections)
96+
97+
98+
# ===== DEMONSTRATE THE INTEGRATION =====
99+
100+
101+
def main():
102+
print("=" * 80)
103+
print("Pydantic-AI Tools Integration Demo")
104+
print("=" * 80)
105+
print()
106+
107+
# Method 1: Pass individual pydantic-ai Tool objects
108+
print("Method 1: Individual Tool objects")
109+
print("-" * 80)
110+
section1 = AnalysisSection(tools=[search_tool, metrics_tool, calc_tool])
111+
print(f"Tools in section: {len(section1.tools)}")
112+
print()
113+
114+
# Method 2: Pass a FunctionToolset (tools are extracted automatically)
115+
print("Method 2: FunctionToolset")
116+
print("-" * 80)
117+
section2 = AnalysisSection(tools=[analysis_toolset])
118+
print(f"Tools in section: {len(section2.tools)}")
119+
print("Tool names:", [t.tool_name for t in section2.tools])
120+
print()
121+
122+
# Method 3: Mix ToolContext and pydantic-ai tools
123+
print("Method 3: Mixed tools (ToolContext + pydantic-ai Tool + FunctionToolset)")
124+
print("-" * 80)
125+
section3 = AnalysisSection(
126+
tools=[
127+
proompt_tool, # Traditional proompt tool
128+
calc_tool, # Individual pydantic-ai Tool
129+
analysis_toolset, # FunctionToolset (extracts multiple tools)
130+
]
131+
)
132+
print(f"Tools in section: {len(section3.tools)}")
133+
print("Tool names:", [t.tool_name for t in section3.tools])
134+
print()
135+
136+
# Create a full prompt
137+
print("Full Prompt Output:")
138+
print("=" * 80)
139+
prompt = MixedToolsPrompt(section3)
140+
print(prompt.render())
141+
print()
142+
143+
# Demonstrate add_tools method
144+
print("Using add_tools method:")
145+
print("-" * 80)
146+
section4 = AnalysisSection()
147+
print(f"Initial tools: {len(section4.tools)}")
148+
149+
section4.add_tools(proompt_tool)
150+
print(f"After adding ToolContext: {len(section4.tools)}")
151+
152+
section4.add_tools(search_tool, metrics_tool)
153+
print(f"After adding pydantic-ai Tools: {len(section4.tools)}")
154+
155+
section4.add_tools(analysis_toolset)
156+
print(f"After adding FunctionToolset: {len(section4.tools)}")
157+
158+
print("Final tool names:", [t.tool_name for t in section4.tools])
159+
print()
160+
161+
# Show tool rendering
162+
print("Individual Tool Rendering:")
163+
print("=" * 80)
164+
for tool in section3.tools[:3]: # Show first 3 tools
165+
print(f"\n{tool.tool_name}:")
166+
print("-" * 40)
167+
print(tool) # automatically uses __str__() to render
168+
169+
170+
if __name__ == "__main__":
171+
main()

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "proompt"
3-
version = "0.1.1"
3+
version = "0.2.0"
44
description = "Object-oriented prompting for Python"
55
authors = [
66
{name = "Burhan Qaddoumi"}
@@ -27,20 +27,24 @@ keywords = [
2727
]
2828
dependencies = [
2929
"pydantic>=2.11.9",
30+
"pydantic-ai-slim>=1.11.0",
3031
]
3132

3233
[project.optional-dependencies]
3334
dev = [
3435
"pylint>=3.3.8",
3536
"pytest>=8.4.2",
3637
"ruff>=0.13.2",
38+
"pytest-cov",
3739
]
3840

3941
[project.urls]
4042
homepage="https://github.com/Burhan-Q/proompt"
4143
source="https://github.com/Burhan-Q/proompt"
4244
issues="https://github.com/Burhan-Q/proompt/issues"
4345

46+
[tool.uv]
47+
package=true
4448

4549
[tool.ruff]
4650
# Exclude a variety of commonly ignored directories.

src/proompt/base/context.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
from textwrap import dedent
44
from typing import Callable
55

6+
from pydantic_ai.tools import Tool
7+
from pydantic_ai.toolsets import FunctionToolset
8+
69

710
class Context(ABC):
811
"""
@@ -29,9 +32,9 @@ class ToolContext(Context):
2932
Attributes:
3033
tool_use (str): Description of how to use the tool.
3134
tool_name (str): Name of the tool.
35+
tool_description (str): Description of the tool's functionality.
3236
tool_args (MappingProxytype): Arguments accepted by the tool.
3337
output_type (Any): Expected output type of the tool.
34-
tool_description (str): Description of the tool's functionality.
3538
3639
Methods:
3740
args_render: renders text for tool arguments
@@ -43,9 +46,53 @@ def __init__(self, tool: Callable, tool_use: str | None = None) -> None:
4346
self._tool = tool
4447
self.tool_use = tool_use or "Reference description for usage."
4548
self.tool_name = tool.__name__
49+
self.tool_description = tool.__doc__ or "No description available."
4650
self.tool_args = inspect.signature(tool).parameters
4751
self.output_type = inspect.signature(tool).return_annotation
48-
self.tool_description = tool.__doc__ or "No description available."
52+
53+
@classmethod
54+
def from_pydantic_tool(cls, tool: Tool) -> "ToolContext":
55+
"""
56+
Create a ToolContext from a pydantic-ai Tool object.
57+
58+
Args:
59+
tool: A pydantic-ai Tool instance
60+
61+
Returns:
62+
ToolContext wrapping the tool's function with its metadata
63+
"""
64+
return cls(tool=tool.function)
65+
66+
@classmethod
67+
def normalize(cls, tool: Callable | "ToolContext" | Tool | FunctionToolset | None) -> list["ToolContext"]:
68+
"""
69+
Normalize any tool type to a list of ToolContext instances.
70+
71+
Accepts:
72+
- ToolContext: Returns as single-item list
73+
- Tool (pydantic-ai): Converts via from_pydantic_tool
74+
- FunctionToolset (pydantic-ai): Extracts all tools from .tools dict
75+
- None or invalid: Returns empty list
76+
77+
Args:
78+
tool (Callable | ToolContext | Tool | FunctionToolset | None): Tool of any supported type
79+
80+
Returns:
81+
List of ToolContext instances (may be empty)
82+
"""
83+
result: list[ToolContext] = []
84+
85+
if isinstance(tool, cls):
86+
result.append(tool)
87+
elif isinstance(tool, Tool):
88+
result.append(cls.from_pydantic_tool(tool))
89+
elif isinstance(tool, FunctionToolset):
90+
for pydantic_tool in tool.tools.values():
91+
result.append(cls.from_pydantic_tool(pydantic_tool))
92+
elif isinstance(tool, Callable):
93+
result.append(cls(tool=tool))
94+
95+
return result
4996

5097
def args_render(self) -> str:
5198
"""Render the tool arguments as a string."""

src/proompt/base/prompt.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@ class PromptSection(ABC):
2525
def __init__(
2626
self,
2727
context: Context | None = None,
28-
tools: list[ToolContext] | None = None,
28+
tools: list | None = None,
2929
*providers: BaseProvider | None,
30-
):
30+
) -> None:
31+
"""Initialize the PromptSection with context, tools, and providers."""
3132
self._context = context
3233
self.providers = list(providers or [])
33-
self.tools = tools or []
34+
self.tools: list[ToolContext] = []
35+
for t in tools or []:
36+
self.tools.extend(ToolContext.normalize(t))
3437

3538
@property
3639
def context(self) -> Context:
@@ -50,9 +53,10 @@ def add_providers(self, *providers: BaseProvider) -> None:
5053
"""Add variable quantity of providers."""
5154
self.providers.extend([p for p in providers if isinstance(p, BaseProvider)])
5255

53-
def add_tools(self, *tools: ToolContext) -> None:
54-
"""Add variable quantity of tools."""
55-
self.tools.extend([t for t in tools if isinstance(t, ToolContext)])
56+
def add_tools(self, *tools) -> None:
57+
"""Add variable quantity of tools (ToolContext, pydantic-ai Tool, or FunctionToolset)."""
58+
for t in tools:
59+
self.tools.extend(ToolContext.normalize(t))
5660

5761
@abstractmethod
5862
def formatter(self, *args, **kwargs) -> str:
@@ -81,6 +85,7 @@ class BasePrompt(ABC):
8185
"""
8286

8387
def __init__(self, *sections: PromptSection) -> None:
88+
"""Initialize the BasePrompt with sections."""
8489
self.sections = list(sections or [])
8590

8691
@abstractmethod

0 commit comments

Comments
 (0)