pactspec-langchain 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pactspec_langchain-0.1.0/.gitignore +53 -0
- pactspec_langchain-0.1.0/PKG-INFO +221 -0
- pactspec_langchain-0.1.0/README.md +195 -0
- pactspec_langchain-0.1.0/pactspec_langchain/__init__.py +27 -0
- pactspec_langchain-0.1.0/pactspec_langchain/toolkit.py +272 -0
- pactspec_langchain-0.1.0/pactspec_langchain/tools.py +193 -0
- pactspec_langchain-0.1.0/pactspec_langchain/types.py +64 -0
- pactspec_langchain-0.1.0/pyproject.toml +40 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# dependencies
|
|
4
|
+
/node_modules
|
|
5
|
+
/.pnp
|
|
6
|
+
.pnp.*
|
|
7
|
+
.yarn/*
|
|
8
|
+
!.yarn/patches
|
|
9
|
+
!.yarn/plugins
|
|
10
|
+
!.yarn/releases
|
|
11
|
+
!.yarn/versions
|
|
12
|
+
|
|
13
|
+
# testing
|
|
14
|
+
/coverage
|
|
15
|
+
/.test-build
|
|
16
|
+
|
|
17
|
+
# next.js
|
|
18
|
+
/.next/
|
|
19
|
+
/out/
|
|
20
|
+
|
|
21
|
+
# production
|
|
22
|
+
/build
|
|
23
|
+
|
|
24
|
+
# misc
|
|
25
|
+
.DS_Store
|
|
26
|
+
*.pem
|
|
27
|
+
|
|
28
|
+
# debug
|
|
29
|
+
npm-debug.log*
|
|
30
|
+
yarn-debug.log*
|
|
31
|
+
yarn-error.log*
|
|
32
|
+
.pnpm-debug.log*
|
|
33
|
+
|
|
34
|
+
# env files (can opt-in for committing if needed)
|
|
35
|
+
.env*
|
|
36
|
+
|
|
37
|
+
# vercel
|
|
38
|
+
.vercel
|
|
39
|
+
|
|
40
|
+
# typescript
|
|
41
|
+
*.tsbuildinfo
|
|
42
|
+
next-env.d.ts
|
|
43
|
+
.env.local
|
|
44
|
+
cli/node_modules/
|
|
45
|
+
cli/dist/
|
|
46
|
+
sdk/node_modules/
|
|
47
|
+
sdk/dist/
|
|
48
|
+
packages/*/dist/
|
|
49
|
+
packages/*/node_modules/
|
|
50
|
+
pactspec-py/dist/
|
|
51
|
+
pactspec-py/*.egg-info/
|
|
52
|
+
pactspec-py/__pycache__/
|
|
53
|
+
cli/test-source.*
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pactspec-langchain
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain integration for PactSpec — discover and invoke verified AI agents as LangChain tools
|
|
5
|
+
Project-URL: Homepage, https://pactspec.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/pactspec/pactspec
|
|
7
|
+
Project-URL: Documentation, https://pactspec.dev/docs/integrations/langchain
|
|
8
|
+
Author-email: PactSpec <hello@pactspec.dev>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agents,ai,langchain,pactspec,tools
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: httpx>=0.25.0
|
|
23
|
+
Requires-Dist: langchain-core>=0.3.0
|
|
24
|
+
Requires-Dist: pactspec>=0.1.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# pactspec-langchain
|
|
28
|
+
|
|
29
|
+
LangChain integration for [PactSpec](https://pactspec.dev) -- discover and invoke verified AI agents as LangChain tools with automatic pricing awareness.
|
|
30
|
+
|
|
31
|
+
Each skill in a PactSpec agent becomes a LangChain `BaseTool` with its name, description, input schema, and pricing information derived from the spec. The LLM sees pricing in the tool description and can make cost-aware decisions.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install pactspec-langchain
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from pactspec_langchain import PactSpecToolkit
|
|
43
|
+
|
|
44
|
+
# Discover verified agents for a task
|
|
45
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
46
|
+
query="invoice processing",
|
|
47
|
+
verified_only=True,
|
|
48
|
+
max_price=0.10,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# List what was found
|
|
52
|
+
for tool in toolkit.get_tools():
|
|
53
|
+
print(f"{tool.name}: {tool.description}")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Using with a LangChain agent
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from langchain_openai import ChatOpenAI
|
|
60
|
+
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
|
61
|
+
from langchain.agents import create_tool_calling_agent, AgentExecutor
|
|
62
|
+
from pactspec_langchain import PactSpecToolkit
|
|
63
|
+
|
|
64
|
+
# 1. Discover tools from the registry
|
|
65
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
66
|
+
query="document analysis",
|
|
67
|
+
verified_only=True,
|
|
68
|
+
max_price=0.25,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# 2. Set up the LLM and prompt
|
|
72
|
+
llm = ChatOpenAI(model="gpt-4o")
|
|
73
|
+
prompt = ChatPromptTemplate.from_messages([
|
|
74
|
+
("system", "You are a helpful assistant. Use the available tools to help the user. "
|
|
75
|
+
"Pay attention to tool costs and prefer cheaper options when quality is similar."),
|
|
76
|
+
("human", "{input}"),
|
|
77
|
+
MessagesPlaceholder("agent_scratchpad"),
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
# 3. Create the agent
|
|
81
|
+
agent = create_tool_calling_agent(llm, toolkit.get_tools(), prompt)
|
|
82
|
+
executor = AgentExecutor(agent=agent, tools=toolkit.get_tools(), verbose=True)
|
|
83
|
+
|
|
84
|
+
# 4. Run it
|
|
85
|
+
result = executor.invoke({"input": "Extract line items from this invoice: ..."})
|
|
86
|
+
print(result["output"])
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Load a specific agent
|
|
90
|
+
|
|
91
|
+
If you know the exact agent you want, load it by spec ID:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
toolkit = PactSpecToolkit.from_agent(
|
|
95
|
+
"urn:pactspec:acme/invoice-agent@1.0.0",
|
|
96
|
+
auth_headers={"Authorization": "Bearer sk-your-api-key"},
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Filtering
|
|
101
|
+
|
|
102
|
+
### By price
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# Only tools that cost at most $0.05 per invocation
|
|
106
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
107
|
+
query="translation",
|
|
108
|
+
max_price=0.05,
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### By pricing model
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# Only free tools
|
|
116
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
117
|
+
query="summarization",
|
|
118
|
+
pricing_model="free",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Only per-token pricing
|
|
122
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
123
|
+
query="summarization",
|
|
124
|
+
pricing_model="per-token",
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Verified agents only
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
# Only agents that have passed PactSpec verification
|
|
132
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
133
|
+
query="code review",
|
|
134
|
+
verified_only=True,
|
|
135
|
+
)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Combining filters
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
142
|
+
query="medical coding",
|
|
143
|
+
verified_only=True,
|
|
144
|
+
max_price=0.10,
|
|
145
|
+
pricing_model="per-invocation",
|
|
146
|
+
)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## How pricing appears in tool descriptions
|
|
150
|
+
|
|
151
|
+
Each tool's description includes pricing and verification status so the LLM can reason about costs:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
Extract line items from invoices | Cost: 0.05 USD/per-invocation via stripe | [Verified] | Agent: InvoiceBot v2.1.0
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
For free tools:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
Summarize text documents | Cost: Free | Agent: SummaryAgent v1.0.0
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Authentication
|
|
164
|
+
|
|
165
|
+
Pass headers for authenticated agent endpoints:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
169
|
+
query="invoice processing",
|
|
170
|
+
auth_headers={
|
|
171
|
+
"Authorization": "Bearer sk-your-key",
|
|
172
|
+
"X-API-Key": "your-api-key",
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Using pre-fetched agents
|
|
178
|
+
|
|
179
|
+
If you've already fetched agents via the PactSpec Python SDK, avoid a second network call:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from pactspec import PactSpecClient
|
|
183
|
+
from pactspec_langchain import PactSpecToolkit
|
|
184
|
+
|
|
185
|
+
client = PactSpecClient()
|
|
186
|
+
result = client.search(q="translation", verified_only=True)
|
|
187
|
+
|
|
188
|
+
# Pass agent records directly
|
|
189
|
+
toolkit = PactSpecToolkit.from_agents(
|
|
190
|
+
result.agents,
|
|
191
|
+
max_price=0.10,
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## API reference
|
|
196
|
+
|
|
197
|
+
### `PactSpecToolkit`
|
|
198
|
+
|
|
199
|
+
| Method | Description |
|
|
200
|
+
|--------|-------------|
|
|
201
|
+
| `from_registry(query, ...)` | Search the registry and create tools from matching agents |
|
|
202
|
+
| `from_agent(spec_id, ...)` | Create tools from a specific agent by spec ID or UUID |
|
|
203
|
+
| `from_agents(agents, ...)` | Create tools from pre-fetched `AgentRecord` objects |
|
|
204
|
+
| `get_tools()` | Return all tools (standard LangChain toolkit interface) |
|
|
205
|
+
| `get_tool(name)` | Look up a single tool by name |
|
|
206
|
+
| `tool_names` | List of all tool names |
|
|
207
|
+
|
|
208
|
+
### `PactSpecTool`
|
|
209
|
+
|
|
210
|
+
Extends `langchain_core.tools.BaseTool`. Each instance wraps a single PactSpec agent skill.
|
|
211
|
+
|
|
212
|
+
| Attribute | Description |
|
|
213
|
+
|-----------|-------------|
|
|
214
|
+
| `agent_meta` | `AgentMetadata` with agent ID, endpoint, verified status |
|
|
215
|
+
| `skill_meta` | `SkillMetadata` with skill ID, description, pricing, input schema |
|
|
216
|
+
| `timeout` | HTTP timeout in seconds (default: 30) |
|
|
217
|
+
| `auth_headers` | Extra headers sent with every invocation |
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# pactspec-langchain
|
|
2
|
+
|
|
3
|
+
LangChain integration for [PactSpec](https://pactspec.dev) -- discover and invoke verified AI agents as LangChain tools with automatic pricing awareness.
|
|
4
|
+
|
|
5
|
+
Each skill in a PactSpec agent becomes a LangChain `BaseTool` with its name, description, input schema, and pricing information derived from the spec. The LLM sees pricing in the tool description and can make cost-aware decisions.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install pactspec-langchain
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from pactspec_langchain import PactSpecToolkit
|
|
17
|
+
|
|
18
|
+
# Discover verified agents for a task
|
|
19
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
20
|
+
query="invoice processing",
|
|
21
|
+
verified_only=True,
|
|
22
|
+
max_price=0.10,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# List what was found
|
|
26
|
+
for tool in toolkit.get_tools():
|
|
27
|
+
print(f"{tool.name}: {tool.description}")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Using with a LangChain agent
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from langchain_openai import ChatOpenAI
|
|
34
|
+
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
|
35
|
+
from langchain.agents import create_tool_calling_agent, AgentExecutor
|
|
36
|
+
from pactspec_langchain import PactSpecToolkit
|
|
37
|
+
|
|
38
|
+
# 1. Discover tools from the registry
|
|
39
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
40
|
+
query="document analysis",
|
|
41
|
+
verified_only=True,
|
|
42
|
+
max_price=0.25,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# 2. Set up the LLM and prompt
|
|
46
|
+
llm = ChatOpenAI(model="gpt-4o")
|
|
47
|
+
prompt = ChatPromptTemplate.from_messages([
|
|
48
|
+
("system", "You are a helpful assistant. Use the available tools to help the user. "
|
|
49
|
+
"Pay attention to tool costs and prefer cheaper options when quality is similar."),
|
|
50
|
+
("human", "{input}"),
|
|
51
|
+
MessagesPlaceholder("agent_scratchpad"),
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
# 3. Create the agent
|
|
55
|
+
agent = create_tool_calling_agent(llm, toolkit.get_tools(), prompt)
|
|
56
|
+
executor = AgentExecutor(agent=agent, tools=toolkit.get_tools(), verbose=True)
|
|
57
|
+
|
|
58
|
+
# 4. Run it
|
|
59
|
+
result = executor.invoke({"input": "Extract line items from this invoice: ..."})
|
|
60
|
+
print(result["output"])
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Load a specific agent
|
|
64
|
+
|
|
65
|
+
If you know the exact agent you want, load it by spec ID:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
toolkit = PactSpecToolkit.from_agent(
|
|
69
|
+
"urn:pactspec:acme/invoice-agent@1.0.0",
|
|
70
|
+
auth_headers={"Authorization": "Bearer sk-your-api-key"},
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Filtering
|
|
75
|
+
|
|
76
|
+
### By price
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# Only tools that cost at most $0.05 per invocation
|
|
80
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
81
|
+
query="translation",
|
|
82
|
+
max_price=0.05,
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### By pricing model
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# Only free tools
|
|
90
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
91
|
+
query="summarization",
|
|
92
|
+
pricing_model="free",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Only per-token pricing
|
|
96
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
97
|
+
query="summarization",
|
|
98
|
+
pricing_model="per-token",
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Verified agents only
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# Only agents that have passed PactSpec verification
|
|
106
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
107
|
+
query="code review",
|
|
108
|
+
verified_only=True,
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Combining filters
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
116
|
+
query="medical coding",
|
|
117
|
+
verified_only=True,
|
|
118
|
+
max_price=0.10,
|
|
119
|
+
pricing_model="per-invocation",
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## How pricing appears in tool descriptions
|
|
124
|
+
|
|
125
|
+
Each tool's description includes pricing and verification status so the LLM can reason about costs:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
Extract line items from invoices | Cost: 0.05 USD/per-invocation via stripe | [Verified] | Agent: InvoiceBot v2.1.0
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
For free tools:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
Summarize text documents | Cost: Free | Agent: SummaryAgent v1.0.0
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Authentication
|
|
138
|
+
|
|
139
|
+
Pass headers for authenticated agent endpoints:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
143
|
+
query="invoice processing",
|
|
144
|
+
auth_headers={
|
|
145
|
+
"Authorization": "Bearer sk-your-key",
|
|
146
|
+
"X-API-Key": "your-api-key",
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Using pre-fetched agents
|
|
152
|
+
|
|
153
|
+
If you've already fetched agents via the PactSpec Python SDK, avoid a second network call:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from pactspec import PactSpecClient
|
|
157
|
+
from pactspec_langchain import PactSpecToolkit
|
|
158
|
+
|
|
159
|
+
client = PactSpecClient()
|
|
160
|
+
result = client.search(q="translation", verified_only=True)
|
|
161
|
+
|
|
162
|
+
# Pass agent records directly
|
|
163
|
+
toolkit = PactSpecToolkit.from_agents(
|
|
164
|
+
result.agents,
|
|
165
|
+
max_price=0.10,
|
|
166
|
+
)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## API reference
|
|
170
|
+
|
|
171
|
+
### `PactSpecToolkit`
|
|
172
|
+
|
|
173
|
+
| Method | Description |
|
|
174
|
+
|--------|-------------|
|
|
175
|
+
| `from_registry(query, ...)` | Search the registry and create tools from matching agents |
|
|
176
|
+
| `from_agent(spec_id, ...)` | Create tools from a specific agent by spec ID or UUID |
|
|
177
|
+
| `from_agents(agents, ...)` | Create tools from pre-fetched `AgentRecord` objects |
|
|
178
|
+
| `get_tools()` | Return all tools (standard LangChain toolkit interface) |
|
|
179
|
+
| `get_tool(name)` | Look up a single tool by name |
|
|
180
|
+
| `tool_names` | List of all tool names |
|
|
181
|
+
|
|
182
|
+
### `PactSpecTool`
|
|
183
|
+
|
|
184
|
+
Extends `langchain_core.tools.BaseTool`. Each instance wraps a single PactSpec agent skill.
|
|
185
|
+
|
|
186
|
+
| Attribute | Description |
|
|
187
|
+
|-----------|-------------|
|
|
188
|
+
| `agent_meta` | `AgentMetadata` with agent ID, endpoint, verified status |
|
|
189
|
+
| `skill_meta` | `SkillMetadata` with skill ID, description, pricing, input schema |
|
|
190
|
+
| `timeout` | HTTP timeout in seconds (default: 30) |
|
|
191
|
+
| `auth_headers` | Extra headers sent with every invocation |
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""PactSpec LangChain integration — use PactSpec agents as LangChain tools.
|
|
2
|
+
|
|
3
|
+
Quick start::
|
|
4
|
+
|
|
5
|
+
from pactspec_langchain import PactSpecToolkit
|
|
6
|
+
|
|
7
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
8
|
+
query="invoice processing",
|
|
9
|
+
verified_only=True,
|
|
10
|
+
max_price=0.10,
|
|
11
|
+
)
|
|
12
|
+
tools = toolkit.get_tools()
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .toolkit import PactSpecToolkit
|
|
16
|
+
from .tools import PactSpecTool
|
|
17
|
+
from .types import AgentMetadata, SkillMetadata, SkillPricing
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"PactSpecToolkit",
|
|
23
|
+
"PactSpecTool",
|
|
24
|
+
"AgentMetadata",
|
|
25
|
+
"SkillMetadata",
|
|
26
|
+
"SkillPricing",
|
|
27
|
+
]
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""PactSpec LangChain toolkit — discover agents from the registry and use them as tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
7
|
+
|
|
8
|
+
from pactspec import PactSpecClient, AgentRecord
|
|
9
|
+
|
|
10
|
+
from .tools import PactSpecTool
|
|
11
|
+
from .types import AgentMetadata, SkillMetadata, SkillPricing
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _extract_tools_from_agent(
|
|
17
|
+
agent: AgentRecord,
|
|
18
|
+
*,
|
|
19
|
+
max_price: Optional[float] = None,
|
|
20
|
+
pricing_model: Optional[str] = None,
|
|
21
|
+
auth_headers: Optional[Dict[str, str]] = None,
|
|
22
|
+
timeout: float = 30.0,
|
|
23
|
+
) -> List[PactSpecTool]:
|
|
24
|
+
"""Extract PactSpecTool instances from a single AgentRecord.
|
|
25
|
+
|
|
26
|
+
Applies pricing / model filters at the skill level.
|
|
27
|
+
"""
|
|
28
|
+
spec = agent.spec or {}
|
|
29
|
+
skills: List[Dict[str, Any]] = spec.get("skills", [])
|
|
30
|
+
|
|
31
|
+
agent_meta = AgentMetadata(
|
|
32
|
+
agent_id=agent.id,
|
|
33
|
+
spec_id=agent.spec_id,
|
|
34
|
+
name=agent.name,
|
|
35
|
+
version=agent.version,
|
|
36
|
+
endpoint_url=agent.endpoint_url,
|
|
37
|
+
verified=agent.verified,
|
|
38
|
+
attestation_hash=agent.attestation_hash,
|
|
39
|
+
provider_name=agent.provider_name,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
tools: List[PactSpecTool] = []
|
|
43
|
+
for skill in skills:
|
|
44
|
+
pricing_raw = skill.get("pricing", {})
|
|
45
|
+
pricing = SkillPricing.from_dict(pricing_raw)
|
|
46
|
+
|
|
47
|
+
# --- filters ---
|
|
48
|
+
if pricing_model is not None and pricing.model != pricing_model:
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
if max_price is not None and pricing.model != "free":
|
|
52
|
+
if pricing.amount > max_price:
|
|
53
|
+
logger.debug(
|
|
54
|
+
"Skipping skill %s: price %.4f > max_price %.4f",
|
|
55
|
+
skill.get("id"),
|
|
56
|
+
pricing.amount,
|
|
57
|
+
max_price,
|
|
58
|
+
)
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
skill_meta = SkillMetadata(
|
|
62
|
+
skill_id=skill.get("id", skill.get("name", "unknown")),
|
|
63
|
+
skill_name=skill.get("name", skill.get("id", "")),
|
|
64
|
+
description=skill.get("description", ""),
|
|
65
|
+
input_schema=skill.get("inputSchema", {}),
|
|
66
|
+
output_schema=skill.get("outputSchema", {}),
|
|
67
|
+
pricing=pricing,
|
|
68
|
+
tags=skill.get("tags", []),
|
|
69
|
+
examples=skill.get("examples", []),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
tool = PactSpecTool(
|
|
73
|
+
agent_meta=agent_meta,
|
|
74
|
+
skill_meta=skill_meta,
|
|
75
|
+
timeout=timeout,
|
|
76
|
+
auth_headers=auth_headers or {},
|
|
77
|
+
)
|
|
78
|
+
tools.append(tool)
|
|
79
|
+
|
|
80
|
+
return tools
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class PactSpecToolkit:
|
|
84
|
+
"""Discover PactSpec agents from the registry and expose them as LangChain tools.
|
|
85
|
+
|
|
86
|
+
Usage::
|
|
87
|
+
|
|
88
|
+
from pactspec_langchain import PactSpecToolkit
|
|
89
|
+
|
|
90
|
+
toolkit = PactSpecToolkit.from_registry(
|
|
91
|
+
query="invoice processing",
|
|
92
|
+
verified_only=True,
|
|
93
|
+
max_price=0.10,
|
|
94
|
+
)
|
|
95
|
+
tools = toolkit.get_tools()
|
|
96
|
+
|
|
97
|
+
Each skill in each matched agent becomes a separate :class:`PactSpecTool`.
|
|
98
|
+
Pricing information is embedded in the tool description so the LLM can make
|
|
99
|
+
cost-aware decisions.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, tools: List[PactSpecTool]) -> None:
|
|
103
|
+
self._tools = list(tools)
|
|
104
|
+
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
# Factory: search the registry
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def from_registry(
|
|
111
|
+
cls,
|
|
112
|
+
query: str = "",
|
|
113
|
+
*,
|
|
114
|
+
verified_only: bool = False,
|
|
115
|
+
max_price: Optional[float] = None,
|
|
116
|
+
pricing_model: Optional[str] = None,
|
|
117
|
+
registry: str = "https://pactspec.dev",
|
|
118
|
+
limit: int = 10,
|
|
119
|
+
auth_headers: Optional[Dict[str, str]] = None,
|
|
120
|
+
timeout: float = 30.0,
|
|
121
|
+
) -> "PactSpecToolkit":
|
|
122
|
+
"""Search the PactSpec registry and build tools from matching agents.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
query: Free-text search query (e.g. ``"invoice processing"``).
|
|
126
|
+
verified_only: Only include agents that have passed verification.
|
|
127
|
+
max_price: Maximum price per invocation — skills above this are excluded.
|
|
128
|
+
pricing_model: Only include skills with this pricing model
|
|
129
|
+
(``"free"``, ``"per-invocation"``, ``"per-token"``, ``"per-second"``).
|
|
130
|
+
registry: PactSpec registry URL.
|
|
131
|
+
limit: Maximum number of agents to fetch.
|
|
132
|
+
auth_headers: Extra HTTP headers to send when invoking agent endpoints
|
|
133
|
+
(e.g. ``{"Authorization": "Bearer sk-..."}``).
|
|
134
|
+
timeout: HTTP timeout in seconds for agent invocations.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
A :class:`PactSpecToolkit` containing one tool per qualifying skill.
|
|
138
|
+
"""
|
|
139
|
+
client = PactSpecClient(registry=registry)
|
|
140
|
+
result = client.search(q=query, verified_only=verified_only, limit=limit)
|
|
141
|
+
|
|
142
|
+
tools: List[PactSpecTool] = []
|
|
143
|
+
for agent in result.agents:
|
|
144
|
+
tools.extend(
|
|
145
|
+
_extract_tools_from_agent(
|
|
146
|
+
agent,
|
|
147
|
+
max_price=max_price,
|
|
148
|
+
pricing_model=pricing_model,
|
|
149
|
+
auth_headers=auth_headers,
|
|
150
|
+
timeout=timeout,
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
logger.info(
|
|
155
|
+
"PactSpecToolkit.from_registry: query=%r, found %d agents, %d tools",
|
|
156
|
+
query,
|
|
157
|
+
len(result.agents),
|
|
158
|
+
len(tools),
|
|
159
|
+
)
|
|
160
|
+
return cls(tools)
|
|
161
|
+
|
|
162
|
+
# ------------------------------------------------------------------
|
|
163
|
+
# Factory: from a specific agent
|
|
164
|
+
# ------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def from_agent(
|
|
168
|
+
cls,
|
|
169
|
+
spec_id: str,
|
|
170
|
+
*,
|
|
171
|
+
max_price: Optional[float] = None,
|
|
172
|
+
pricing_model: Optional[str] = None,
|
|
173
|
+
registry: str = "https://pactspec.dev",
|
|
174
|
+
auth_headers: Optional[Dict[str, str]] = None,
|
|
175
|
+
timeout: float = 30.0,
|
|
176
|
+
) -> "PactSpecToolkit":
|
|
177
|
+
"""Create tools from a specific agent by its spec ID or registry UUID.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
spec_id: The agent's spec URN (e.g. ``"urn:pactspec:acme/invoice-agent@1.0.0"``)
|
|
181
|
+
or registry UUID.
|
|
182
|
+
max_price: Maximum price per invocation.
|
|
183
|
+
pricing_model: Only include skills with this pricing model.
|
|
184
|
+
registry: PactSpec registry URL.
|
|
185
|
+
auth_headers: Extra HTTP headers for agent invocations.
|
|
186
|
+
timeout: HTTP timeout in seconds for agent invocations.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
A :class:`PactSpecToolkit` containing one tool per qualifying skill.
|
|
190
|
+
"""
|
|
191
|
+
client = PactSpecClient(registry=registry)
|
|
192
|
+
agent = client.get_agent(spec_id)
|
|
193
|
+
|
|
194
|
+
tools = _extract_tools_from_agent(
|
|
195
|
+
agent,
|
|
196
|
+
max_price=max_price,
|
|
197
|
+
pricing_model=pricing_model,
|
|
198
|
+
auth_headers=auth_headers,
|
|
199
|
+
timeout=timeout,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
logger.info(
|
|
203
|
+
"PactSpecToolkit.from_agent: spec_id=%r, %d tools",
|
|
204
|
+
spec_id,
|
|
205
|
+
len(tools),
|
|
206
|
+
)
|
|
207
|
+
return cls(tools)
|
|
208
|
+
|
|
209
|
+
# ------------------------------------------------------------------
|
|
210
|
+
# Factory: from raw agent dicts (for advanced use)
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def from_agents(
|
|
215
|
+
cls,
|
|
216
|
+
agents: Sequence[AgentRecord],
|
|
217
|
+
*,
|
|
218
|
+
max_price: Optional[float] = None,
|
|
219
|
+
pricing_model: Optional[str] = None,
|
|
220
|
+
auth_headers: Optional[Dict[str, str]] = None,
|
|
221
|
+
timeout: float = 30.0,
|
|
222
|
+
) -> "PactSpecToolkit":
|
|
223
|
+
"""Create a toolkit from a pre-fetched list of :class:`AgentRecord` objects.
|
|
224
|
+
|
|
225
|
+
This is useful when you've already fetched agents and want to avoid
|
|
226
|
+
a second network call.
|
|
227
|
+
"""
|
|
228
|
+
tools: List[PactSpecTool] = []
|
|
229
|
+
for agent in agents:
|
|
230
|
+
tools.extend(
|
|
231
|
+
_extract_tools_from_agent(
|
|
232
|
+
agent,
|
|
233
|
+
max_price=max_price,
|
|
234
|
+
pricing_model=pricing_model,
|
|
235
|
+
auth_headers=auth_headers,
|
|
236
|
+
timeout=timeout,
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
return cls(tools)
|
|
240
|
+
|
|
241
|
+
# ------------------------------------------------------------------
|
|
242
|
+
# Tool access
|
|
243
|
+
# ------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
def get_tools(self) -> List[PactSpecTool]:
|
|
246
|
+
"""Return all tools in the toolkit.
|
|
247
|
+
|
|
248
|
+
This is the standard LangChain toolkit interface used by
|
|
249
|
+
``create_tool_calling_agent`` and similar helpers.
|
|
250
|
+
"""
|
|
251
|
+
return list(self._tools)
|
|
252
|
+
|
|
253
|
+
def get_tool(self, name: str) -> Optional[PactSpecTool]:
|
|
254
|
+
"""Look up a single tool by name.
|
|
255
|
+
|
|
256
|
+
Returns ``None`` if no tool with that name exists.
|
|
257
|
+
"""
|
|
258
|
+
for tool in self._tools:
|
|
259
|
+
if tool.name == name:
|
|
260
|
+
return tool
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def tool_names(self) -> List[str]:
|
|
265
|
+
"""List the names of all tools in the toolkit."""
|
|
266
|
+
return [t.name for t in self._tools]
|
|
267
|
+
|
|
268
|
+
def __len__(self) -> int:
|
|
269
|
+
return len(self._tools)
|
|
270
|
+
|
|
271
|
+
def __repr__(self) -> str:
|
|
272
|
+
return f"PactSpecToolkit(tools={self.tool_names})"
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""PactSpec LangChain tool — wraps a single PactSpec agent skill as a LangChain BaseTool."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Dict, Optional, Type
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from langchain_core.callbacks import (
|
|
11
|
+
AsyncCallbackManagerForToolRun,
|
|
12
|
+
CallbackManagerForToolRun,
|
|
13
|
+
)
|
|
14
|
+
from langchain_core.tools import BaseTool
|
|
15
|
+
from pydantic import BaseModel, Field, model_validator
|
|
16
|
+
|
|
17
|
+
from .types import AgentMetadata, SkillMetadata, SkillPricing
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _build_pydantic_model_from_json_schema(
|
|
23
|
+
schema: Dict[str, Any],
|
|
24
|
+
model_name: str = "DynamicInput",
|
|
25
|
+
) -> Type[BaseModel]:
|
|
26
|
+
"""Build a pydantic model from a JSON Schema dict.
|
|
27
|
+
|
|
28
|
+
This creates a model with fields derived from the JSON Schema ``properties``.
|
|
29
|
+
For properties without an explicit type we fall back to ``Any``.
|
|
30
|
+
"""
|
|
31
|
+
properties = schema.get("properties", {})
|
|
32
|
+
required = set(schema.get("required", []))
|
|
33
|
+
|
|
34
|
+
field_definitions: Dict[str, Any] = {}
|
|
35
|
+
annotations: Dict[str, Any] = {}
|
|
36
|
+
|
|
37
|
+
type_map = {
|
|
38
|
+
"string": str,
|
|
39
|
+
"integer": int,
|
|
40
|
+
"number": float,
|
|
41
|
+
"boolean": bool,
|
|
42
|
+
"array": list,
|
|
43
|
+
"object": dict,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for prop_name, prop_schema in properties.items():
|
|
47
|
+
json_type = prop_schema.get("type", "string")
|
|
48
|
+
python_type = type_map.get(json_type, Any)
|
|
49
|
+
description = prop_schema.get("description", "")
|
|
50
|
+
default = prop_schema.get("default")
|
|
51
|
+
|
|
52
|
+
if prop_name in required and default is None:
|
|
53
|
+
field_definitions[prop_name] = Field(description=description)
|
|
54
|
+
else:
|
|
55
|
+
if python_type is Any:
|
|
56
|
+
annotations[prop_name] = Optional[Any]
|
|
57
|
+
else:
|
|
58
|
+
annotations[prop_name] = Optional[python_type] # type: ignore[assignment]
|
|
59
|
+
field_definitions[prop_name] = Field(default=default, description=description)
|
|
60
|
+
|
|
61
|
+
if prop_name not in annotations:
|
|
62
|
+
annotations[prop_name] = python_type
|
|
63
|
+
|
|
64
|
+
namespace: Dict[str, Any] = {"__annotations__": annotations, **field_definitions}
|
|
65
|
+
model = type(model_name, (BaseModel,), namespace)
|
|
66
|
+
return model # type: ignore[return-value]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class PactSpecTool(BaseTool):
|
|
70
|
+
"""A LangChain tool backed by a single PactSpec agent skill.
|
|
71
|
+
|
|
72
|
+
Each skill declared in a PactSpec becomes one ``PactSpecTool`` instance.
|
|
73
|
+
The tool name, description, and input schema are derived from the spec,
|
|
74
|
+
and the ``_run`` / ``_arun`` methods invoke the agent endpoint directly.
|
|
75
|
+
|
|
76
|
+
Pricing information is appended to the tool description so that the LLM
|
|
77
|
+
can make cost-aware decisions when selecting tools.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
# --- metadata stored on the tool ---
|
|
81
|
+
agent_meta: AgentMetadata
|
|
82
|
+
skill_meta: SkillMetadata
|
|
83
|
+
|
|
84
|
+
# --- httpx settings ---
|
|
85
|
+
timeout: float = 30.0
|
|
86
|
+
auth_headers: Dict[str, str] = Field(default_factory=dict)
|
|
87
|
+
|
|
88
|
+
# These are set via model_validator from the metadata
|
|
89
|
+
name: str = "" # type: ignore[assignment]
|
|
90
|
+
description: str = ""
|
|
91
|
+
|
|
92
|
+
args_schema: Optional[Type[BaseModel]] = None # type: ignore[assignment]
|
|
93
|
+
|
|
94
|
+
@model_validator(mode="after")
|
|
95
|
+
def _set_derived_fields(self) -> "PactSpecTool":
|
|
96
|
+
# Tool name: use skill_id, sanitised for LangChain (alphanumeric + hyphens/underscores)
|
|
97
|
+
if not self.name:
|
|
98
|
+
self.name = self.skill_meta.skill_id.replace(" ", "_")
|
|
99
|
+
|
|
100
|
+
# Build a rich description with pricing and verification status
|
|
101
|
+
if not self.description:
|
|
102
|
+
parts = [self.skill_meta.description or self.skill_meta.skill_name]
|
|
103
|
+
pricing_display = self.skill_meta.pricing.display
|
|
104
|
+
if pricing_display:
|
|
105
|
+
parts.append(f"Cost: {pricing_display}")
|
|
106
|
+
if self.agent_meta.verified:
|
|
107
|
+
parts.append("[Verified]")
|
|
108
|
+
parts.append(f"Agent: {self.agent_meta.name} v{self.agent_meta.version}")
|
|
109
|
+
self.description = " | ".join(parts)
|
|
110
|
+
|
|
111
|
+
# Build args_schema from the skill's inputSchema
|
|
112
|
+
if self.args_schema is None and self.skill_meta.input_schema:
|
|
113
|
+
model_name = (
|
|
114
|
+
self.skill_meta.skill_id.replace("-", "_").replace(" ", "_").title().replace("_", "")
|
|
115
|
+
+ "Input"
|
|
116
|
+
)
|
|
117
|
+
self.args_schema = _build_pydantic_model_from_json_schema(
|
|
118
|
+
self.skill_meta.input_schema,
|
|
119
|
+
model_name=model_name,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
# ------------------------------------------------------------------
|
|
125
|
+
# Invocation
|
|
126
|
+
# ------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
def _build_url(self) -> str:
|
|
129
|
+
"""Construct the invocation URL for the agent skill."""
|
|
130
|
+
base = self.agent_meta.endpoint_url.rstrip("/")
|
|
131
|
+
return base
|
|
132
|
+
|
|
133
|
+
def _build_headers(self) -> Dict[str, str]:
|
|
134
|
+
headers = {"Content-Type": "application/json"}
|
|
135
|
+
headers.update(self.auth_headers)
|
|
136
|
+
return headers
|
|
137
|
+
|
|
138
|
+
def _handle_response(self, response: httpx.Response) -> str:
|
|
139
|
+
"""Process the HTTP response and return a string for the LLM."""
|
|
140
|
+
if response.status_code == 402:
|
|
141
|
+
try:
|
|
142
|
+
detail = response.json()
|
|
143
|
+
except Exception:
|
|
144
|
+
detail = response.text
|
|
145
|
+
return f"Payment required for this agent skill. Details: {detail}"
|
|
146
|
+
|
|
147
|
+
response.raise_for_status()
|
|
148
|
+
|
|
149
|
+
# Try to return formatted JSON; fall back to raw text
|
|
150
|
+
content_type = response.headers.get("content-type", "")
|
|
151
|
+
if "json" in content_type:
|
|
152
|
+
try:
|
|
153
|
+
return json.dumps(response.json(), indent=2)
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
return response.text
|
|
157
|
+
|
|
158
|
+
def _run(
|
|
159
|
+
self,
|
|
160
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
161
|
+
**kwargs: Any,
|
|
162
|
+
) -> str:
|
|
163
|
+
"""Synchronously invoke the PactSpec agent skill."""
|
|
164
|
+
url = self._build_url()
|
|
165
|
+
headers = self._build_headers()
|
|
166
|
+
logger.debug("PactSpecTool %s: POST %s with %s", self.name, url, kwargs)
|
|
167
|
+
|
|
168
|
+
response = httpx.post(
|
|
169
|
+
url,
|
|
170
|
+
json=kwargs,
|
|
171
|
+
headers=headers,
|
|
172
|
+
timeout=self.timeout,
|
|
173
|
+
)
|
|
174
|
+
return self._handle_response(response)
|
|
175
|
+
|
|
176
|
+
async def _arun(
|
|
177
|
+
self,
|
|
178
|
+
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
|
|
179
|
+
**kwargs: Any,
|
|
180
|
+
) -> str:
|
|
181
|
+
"""Asynchronously invoke the PactSpec agent skill."""
|
|
182
|
+
url = self._build_url()
|
|
183
|
+
headers = self._build_headers()
|
|
184
|
+
logger.debug("PactSpecTool %s: async POST %s with %s", self.name, url, kwargs)
|
|
185
|
+
|
|
186
|
+
async with httpx.AsyncClient() as client:
|
|
187
|
+
response = await client.post(
|
|
188
|
+
url,
|
|
189
|
+
json=kwargs,
|
|
190
|
+
headers=headers,
|
|
191
|
+
timeout=self.timeout,
|
|
192
|
+
)
|
|
193
|
+
return self._handle_response(response)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Type definitions for the PactSpec LangChain integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SkillPricing(BaseModel):
|
|
11
|
+
"""Pricing information extracted from a PactSpec skill."""
|
|
12
|
+
|
|
13
|
+
model: str = "free"
|
|
14
|
+
amount: float = 0.0
|
|
15
|
+
currency: str = "USD"
|
|
16
|
+
protocol: Optional[str] = None
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def display(self) -> str:
|
|
20
|
+
"""Human-readable pricing string for tool descriptions."""
|
|
21
|
+
if self.model == "free":
|
|
22
|
+
return "Free"
|
|
23
|
+
parts = [f"{self.amount} {self.currency}/{self.model}"]
|
|
24
|
+
if self.protocol and self.protocol != "none":
|
|
25
|
+
parts.append(f"via {self.protocol}")
|
|
26
|
+
return " ".join(parts)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_dict(cls, data: Dict[str, Any]) -> "SkillPricing":
|
|
30
|
+
"""Parse pricing from a raw PactSpec skill pricing dict."""
|
|
31
|
+
if not data:
|
|
32
|
+
return cls()
|
|
33
|
+
return cls(
|
|
34
|
+
model=data.get("model", "free"),
|
|
35
|
+
amount=data.get("amount", 0.0),
|
|
36
|
+
currency=data.get("currency", "USD"),
|
|
37
|
+
protocol=data.get("protocol"),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SkillMetadata(BaseModel):
|
|
42
|
+
"""Metadata about a PactSpec skill used to construct a LangChain tool."""
|
|
43
|
+
|
|
44
|
+
skill_id: str
|
|
45
|
+
skill_name: str
|
|
46
|
+
description: str
|
|
47
|
+
input_schema: Dict[str, Any] = Field(default_factory=dict)
|
|
48
|
+
output_schema: Dict[str, Any] = Field(default_factory=dict)
|
|
49
|
+
pricing: SkillPricing = Field(default_factory=SkillPricing)
|
|
50
|
+
tags: List[str] = Field(default_factory=list)
|
|
51
|
+
examples: List[Dict[str, Any]] = Field(default_factory=list)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AgentMetadata(BaseModel):
|
|
55
|
+
"""Metadata about the parent PactSpec agent for a tool."""
|
|
56
|
+
|
|
57
|
+
agent_id: str
|
|
58
|
+
spec_id: str
|
|
59
|
+
name: str
|
|
60
|
+
version: str
|
|
61
|
+
endpoint_url: str
|
|
62
|
+
verified: bool = False
|
|
63
|
+
attestation_hash: Optional[str] = None
|
|
64
|
+
provider_name: str = ""
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pactspec-langchain"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "LangChain integration for PactSpec — discover and invoke verified AI agents as LangChain tools"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "PactSpec", email = "hello@pactspec.dev" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["pactspec", "langchain", "agents", "tools", "ai"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"pactspec>=0.1.0",
|
|
30
|
+
"langchain-core>=0.3.0",
|
|
31
|
+
"httpx>=0.25.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://pactspec.dev"
|
|
36
|
+
Repository = "https://github.com/pactspec/pactspec"
|
|
37
|
+
Documentation = "https://pactspec.dev/docs/integrations/langchain"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["pactspec_langchain"]
|