apia 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.
- apia-0.1.0/.github/workflows/ci.yml +25 -0
- apia-0.1.0/.github/workflows/publish.yml +49 -0
- apia-0.1.0/.gitignore +11 -0
- apia-0.1.0/PKG-INFO +166 -0
- apia-0.1.0/README.md +137 -0
- apia-0.1.0/apia/__init__.py +27 -0
- apia-0.1.0/apia/exceptions.py +13 -0
- apia-0.1.0/apia/manifest.py +196 -0
- apia-0.1.0/apia/registry.py +176 -0
- apia-0.1.0/pyproject.toml +44 -0
- apia-0.1.0/pytest.ini +3 -0
- apia-0.1.0/tests/test_apia.py +198 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: |
|
|
23
|
+
pip install -e ".[dev]"
|
|
24
|
+
- name: Test
|
|
25
|
+
run: pytest --tb=short -q
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
id-token: write # нужен для trusted publishing (без API токена)
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
- name: Install build tools
|
|
22
|
+
run: pip install build
|
|
23
|
+
|
|
24
|
+
- name: Build wheel and sdist
|
|
25
|
+
run: python -m build
|
|
26
|
+
|
|
27
|
+
- name: Upload dist as artifact
|
|
28
|
+
uses: actions/upload-artifact@v4
|
|
29
|
+
with:
|
|
30
|
+
name: dist
|
|
31
|
+
path: dist/
|
|
32
|
+
|
|
33
|
+
publish-pypi:
|
|
34
|
+
needs: build
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
environment:
|
|
37
|
+
name: pypi
|
|
38
|
+
url: https://pypi.org/project/apia/
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/download-artifact@v4
|
|
41
|
+
with:
|
|
42
|
+
name: dist
|
|
43
|
+
path: dist/
|
|
44
|
+
|
|
45
|
+
- name: Publish to PyPI
|
|
46
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
47
|
+
# Trusted Publishing — не нужен API токен
|
|
48
|
+
# Настроить в PyPI: pypi.org → Account → Publishing
|
|
49
|
+
# Publisher: GitHub Actions, repo: Komsomol39/apia-py, workflow: publish.yml
|
apia-0.1.0/.gitignore
ADDED
apia-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apia
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for APIA — AI-native API manifests standard
|
|
5
|
+
Project-URL: Homepage, https://github.com/Komsomol39/apia-standard
|
|
6
|
+
Project-URL: Repository, https://github.com/Komsomol39/apia-py
|
|
7
|
+
Project-URL: Issues, https://github.com/Komsomol39/apia-py/issues
|
|
8
|
+
Author: APIA Community
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: agents,ai,api,apia,llm,manifests
|
|
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: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: httpx>=0.24.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# apia-py
|
|
31
|
+
|
|
32
|
+
Python SDK for [APIA](https://github.com/Komsomol39/apia-standard) — the open standard for AI-native API manifests.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install apia
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from apia import Registry
|
|
42
|
+
|
|
43
|
+
registry = Registry()
|
|
44
|
+
|
|
45
|
+
# Find APIs for a task
|
|
46
|
+
apis = registry.find("send a telegram message")
|
|
47
|
+
print(apis[0].name) # → Telegram Bot API
|
|
48
|
+
print(apis[0].category) # → social
|
|
49
|
+
|
|
50
|
+
# Get a specific manifest
|
|
51
|
+
manifest = registry.get("stripe")
|
|
52
|
+
print(manifest.service.description_for_ai)
|
|
53
|
+
|
|
54
|
+
# Convert to OpenAI tools
|
|
55
|
+
tools = manifest.to_openai_tools()
|
|
56
|
+
|
|
57
|
+
# Build a system prompt for an LLM
|
|
58
|
+
prompt = registry.build_system_prompt(apis)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Core API
|
|
62
|
+
|
|
63
|
+
### `Registry`
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from apia import Registry
|
|
67
|
+
|
|
68
|
+
r = Registry()
|
|
69
|
+
|
|
70
|
+
# Search by intent (natural language)
|
|
71
|
+
r.find("track DHL package") # → [Manifest, ...]
|
|
72
|
+
r.find("crypto price", category="finance") # → filtered
|
|
73
|
+
|
|
74
|
+
# Load a specific manifest
|
|
75
|
+
r.get("openai") # → Manifest
|
|
76
|
+
|
|
77
|
+
# List with filters
|
|
78
|
+
r.list(category="ai") # all AI APIs
|
|
79
|
+
r.list(geo="RU", free_only=True) # free Russian APIs
|
|
80
|
+
r.list(language="ru") # Russian-language APIs
|
|
81
|
+
|
|
82
|
+
# Categories overview
|
|
83
|
+
r.categories() # → {"ai": 25, "finance": 17, ...}
|
|
84
|
+
|
|
85
|
+
# Build LLM system prompt from multiple manifests
|
|
86
|
+
prompt = r.build_system_prompt(apis)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `Manifest`
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
m = r.get("stripe")
|
|
93
|
+
|
|
94
|
+
m.id # "stripe"
|
|
95
|
+
m.name # "Stripe"
|
|
96
|
+
m.category # "finance"
|
|
97
|
+
m.geo # ["GLOBAL"]
|
|
98
|
+
m.is_free # False
|
|
99
|
+
|
|
100
|
+
# Find capability by task
|
|
101
|
+
cap = m.find_capability("charge a customer")
|
|
102
|
+
cap.id # "create_payment_intent"
|
|
103
|
+
cap.endpoint # "POST https://api.stripe.com/v1/payment_intents"
|
|
104
|
+
|
|
105
|
+
# Export
|
|
106
|
+
m.to_openai_tools() # list of OpenAI function definitions
|
|
107
|
+
m.to_system_prompt() # formatted string for LLM system prompt
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Use with LLMs
|
|
111
|
+
|
|
112
|
+
### Anthropic Claude
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from apia import Registry
|
|
116
|
+
import anthropic
|
|
117
|
+
|
|
118
|
+
r = Registry()
|
|
119
|
+
apis = r.find("send telegram message")
|
|
120
|
+
system = r.build_system_prompt(apis)
|
|
121
|
+
|
|
122
|
+
client = anthropic.Anthropic()
|
|
123
|
+
response = client.messages.create(
|
|
124
|
+
model="claude-haiku-4-5",
|
|
125
|
+
max_tokens=1024,
|
|
126
|
+
system=system,
|
|
127
|
+
messages=[{"role": "user", "content": "Send 'Hello!' to chat 123456"}]
|
|
128
|
+
)
|
|
129
|
+
print(response.content[0].text)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### OpenAI function calling
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from apia import Registry
|
|
136
|
+
import openai
|
|
137
|
+
|
|
138
|
+
r = Registry()
|
|
139
|
+
manifest = r.get("openweathermap")
|
|
140
|
+
tools = manifest.to_openai_tools()
|
|
141
|
+
|
|
142
|
+
client = openai.OpenAI()
|
|
143
|
+
response = client.chat.completions.create(
|
|
144
|
+
model="gpt-4o-mini",
|
|
145
|
+
messages=[{"role": "user", "content": "What's the weather in Tokyo?"}],
|
|
146
|
+
tools=tools,
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Development
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
git clone https://github.com/Komsomol39/apia-py
|
|
154
|
+
cd apia-py
|
|
155
|
+
pip install -e ".[dev]"
|
|
156
|
+
pytest
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Related
|
|
160
|
+
|
|
161
|
+
- [apia-standard](https://github.com/Komsomol39/apia-standard) — manifest registry (257 APIs)
|
|
162
|
+
- [apia-js](https://github.com/Komsomol39/apia-js) — JavaScript/TypeScript SDK
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
MIT
|
apia-0.1.0/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# apia-py
|
|
2
|
+
|
|
3
|
+
Python SDK for [APIA](https://github.com/Komsomol39/apia-standard) — the open standard for AI-native API manifests.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install apia
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from apia import Registry
|
|
13
|
+
|
|
14
|
+
registry = Registry()
|
|
15
|
+
|
|
16
|
+
# Find APIs for a task
|
|
17
|
+
apis = registry.find("send a telegram message")
|
|
18
|
+
print(apis[0].name) # → Telegram Bot API
|
|
19
|
+
print(apis[0].category) # → social
|
|
20
|
+
|
|
21
|
+
# Get a specific manifest
|
|
22
|
+
manifest = registry.get("stripe")
|
|
23
|
+
print(manifest.service.description_for_ai)
|
|
24
|
+
|
|
25
|
+
# Convert to OpenAI tools
|
|
26
|
+
tools = manifest.to_openai_tools()
|
|
27
|
+
|
|
28
|
+
# Build a system prompt for an LLM
|
|
29
|
+
prompt = registry.build_system_prompt(apis)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Core API
|
|
33
|
+
|
|
34
|
+
### `Registry`
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from apia import Registry
|
|
38
|
+
|
|
39
|
+
r = Registry()
|
|
40
|
+
|
|
41
|
+
# Search by intent (natural language)
|
|
42
|
+
r.find("track DHL package") # → [Manifest, ...]
|
|
43
|
+
r.find("crypto price", category="finance") # → filtered
|
|
44
|
+
|
|
45
|
+
# Load a specific manifest
|
|
46
|
+
r.get("openai") # → Manifest
|
|
47
|
+
|
|
48
|
+
# List with filters
|
|
49
|
+
r.list(category="ai") # all AI APIs
|
|
50
|
+
r.list(geo="RU", free_only=True) # free Russian APIs
|
|
51
|
+
r.list(language="ru") # Russian-language APIs
|
|
52
|
+
|
|
53
|
+
# Categories overview
|
|
54
|
+
r.categories() # → {"ai": 25, "finance": 17, ...}
|
|
55
|
+
|
|
56
|
+
# Build LLM system prompt from multiple manifests
|
|
57
|
+
prompt = r.build_system_prompt(apis)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `Manifest`
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
m = r.get("stripe")
|
|
64
|
+
|
|
65
|
+
m.id # "stripe"
|
|
66
|
+
m.name # "Stripe"
|
|
67
|
+
m.category # "finance"
|
|
68
|
+
m.geo # ["GLOBAL"]
|
|
69
|
+
m.is_free # False
|
|
70
|
+
|
|
71
|
+
# Find capability by task
|
|
72
|
+
cap = m.find_capability("charge a customer")
|
|
73
|
+
cap.id # "create_payment_intent"
|
|
74
|
+
cap.endpoint # "POST https://api.stripe.com/v1/payment_intents"
|
|
75
|
+
|
|
76
|
+
# Export
|
|
77
|
+
m.to_openai_tools() # list of OpenAI function definitions
|
|
78
|
+
m.to_system_prompt() # formatted string for LLM system prompt
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Use with LLMs
|
|
82
|
+
|
|
83
|
+
### Anthropic Claude
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from apia import Registry
|
|
87
|
+
import anthropic
|
|
88
|
+
|
|
89
|
+
r = Registry()
|
|
90
|
+
apis = r.find("send telegram message")
|
|
91
|
+
system = r.build_system_prompt(apis)
|
|
92
|
+
|
|
93
|
+
client = anthropic.Anthropic()
|
|
94
|
+
response = client.messages.create(
|
|
95
|
+
model="claude-haiku-4-5",
|
|
96
|
+
max_tokens=1024,
|
|
97
|
+
system=system,
|
|
98
|
+
messages=[{"role": "user", "content": "Send 'Hello!' to chat 123456"}]
|
|
99
|
+
)
|
|
100
|
+
print(response.content[0].text)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### OpenAI function calling
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from apia import Registry
|
|
107
|
+
import openai
|
|
108
|
+
|
|
109
|
+
r = Registry()
|
|
110
|
+
manifest = r.get("openweathermap")
|
|
111
|
+
tools = manifest.to_openai_tools()
|
|
112
|
+
|
|
113
|
+
client = openai.OpenAI()
|
|
114
|
+
response = client.chat.completions.create(
|
|
115
|
+
model="gpt-4o-mini",
|
|
116
|
+
messages=[{"role": "user", "content": "What's the weather in Tokyo?"}],
|
|
117
|
+
tools=tools,
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
git clone https://github.com/Komsomol39/apia-py
|
|
125
|
+
cd apia-py
|
|
126
|
+
pip install -e ".[dev]"
|
|
127
|
+
pytest
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Related
|
|
131
|
+
|
|
132
|
+
- [apia-standard](https://github.com/Komsomol39/apia-standard) — manifest registry (257 APIs)
|
|
133
|
+
- [apia-js](https://github.com/Komsomol39/apia-js) — JavaScript/TypeScript SDK
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
APIA Python SDK
|
|
3
|
+
~~~~~~~~~~~~~~~
|
|
4
|
+
Python client for the APIA standard — AI-native API manifest discovery.
|
|
5
|
+
|
|
6
|
+
>>> from apia import Registry
|
|
7
|
+
>>> registry = Registry()
|
|
8
|
+
>>> apis = registry.find("send telegram message")
|
|
9
|
+
>>> print(apis[0].name)
|
|
10
|
+
'Telegram Bot'
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .registry import Registry
|
|
14
|
+
from .manifest import Manifest, Capability, Service, Auth
|
|
15
|
+
from .exceptions import ApiaError, ManifestNotFoundError, RegistryError
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Registry",
|
|
20
|
+
"Manifest",
|
|
21
|
+
"Capability",
|
|
22
|
+
"Service",
|
|
23
|
+
"Auth",
|
|
24
|
+
"ApiaError",
|
|
25
|
+
"ManifestNotFoundError",
|
|
26
|
+
"RegistryError",
|
|
27
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""APIA exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ApiaError(Exception):
|
|
5
|
+
"""Base exception for all APIA errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RegistryError(ApiaError):
|
|
9
|
+
"""Raised when the registry cannot be loaded or is invalid."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ManifestNotFoundError(ApiaError):
|
|
13
|
+
"""Raised when a manifest cannot be found by the given id or criteria."""
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Data models for APIA manifests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Auth:
|
|
10
|
+
type: str
|
|
11
|
+
anonymous_access: bool
|
|
12
|
+
how_to_get: str = ""
|
|
13
|
+
cost: str = ""
|
|
14
|
+
header: str = ""
|
|
15
|
+
param_name: str = ""
|
|
16
|
+
param_location: str = ""
|
|
17
|
+
token_url: str = ""
|
|
18
|
+
note: str = ""
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_dict(cls, data: dict[str, Any]) -> "Auth":
|
|
22
|
+
return cls(
|
|
23
|
+
type=data.get("type", ""),
|
|
24
|
+
anonymous_access=data.get("anonymous_access", False),
|
|
25
|
+
how_to_get=data.get("how_to_get", ""),
|
|
26
|
+
cost=data.get("cost", ""),
|
|
27
|
+
header=data.get("header", ""),
|
|
28
|
+
param_name=data.get("param_name", ""),
|
|
29
|
+
param_location=data.get("param_location", ""),
|
|
30
|
+
token_url=data.get("token_url", ""),
|
|
31
|
+
note=data.get("note", ""),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Service:
|
|
37
|
+
id: str
|
|
38
|
+
name: str
|
|
39
|
+
description_for_ai: str
|
|
40
|
+
category: str
|
|
41
|
+
geo: list[str]
|
|
42
|
+
language: str = "en"
|
|
43
|
+
url: str = ""
|
|
44
|
+
api_base: str = ""
|
|
45
|
+
docs: str = ""
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_dict(cls, data: dict[str, Any]) -> "Service":
|
|
49
|
+
return cls(
|
|
50
|
+
id=data.get("id", ""),
|
|
51
|
+
name=data.get("name", ""),
|
|
52
|
+
description_for_ai=data.get("description_for_ai", ""),
|
|
53
|
+
category=data.get("category", ""),
|
|
54
|
+
geo=data.get("geo", []),
|
|
55
|
+
language=data.get("language", "en"),
|
|
56
|
+
url=data.get("url", ""),
|
|
57
|
+
api_base=data.get("api_base", ""),
|
|
58
|
+
docs=data.get("docs", ""),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class Capability:
|
|
64
|
+
id: str
|
|
65
|
+
description_for_ai: str
|
|
66
|
+
intent: list[str]
|
|
67
|
+
endpoint: str
|
|
68
|
+
input: dict[str, Any] = field(default_factory=dict)
|
|
69
|
+
output: dict[str, Any] = field(default_factory=dict)
|
|
70
|
+
realtime: bool = False
|
|
71
|
+
requires_auth: bool = True
|
|
72
|
+
rate_limit: str = ""
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_dict(cls, data: dict[str, Any]) -> "Capability":
|
|
76
|
+
return cls(
|
|
77
|
+
id=data.get("id", ""),
|
|
78
|
+
description_for_ai=data.get("description_for_ai", ""),
|
|
79
|
+
intent=data.get("intent", []),
|
|
80
|
+
endpoint=data.get("endpoint", ""),
|
|
81
|
+
input=data.get("input", {}),
|
|
82
|
+
output=data.get("output", {}),
|
|
83
|
+
realtime=data.get("realtime", False),
|
|
84
|
+
requires_auth=data.get("requires_auth", True),
|
|
85
|
+
rate_limit=data.get("rate_limit", ""),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def matches_intent(self, task: str) -> bool:
|
|
89
|
+
"""Return True if any intent phrase matches the task string."""
|
|
90
|
+
task_lower = task.lower()
|
|
91
|
+
return any(
|
|
92
|
+
task_lower in phrase.lower() or phrase.lower() in task_lower
|
|
93
|
+
for phrase in self.intent
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def to_openai_tool(self) -> dict[str, Any]:
|
|
97
|
+
"""Convert this capability to an OpenAI function/tool definition."""
|
|
98
|
+
properties: dict[str, Any] = {}
|
|
99
|
+
required: list[str] = []
|
|
100
|
+
for name, spec in self.input.items():
|
|
101
|
+
if not isinstance(spec, dict):
|
|
102
|
+
continue
|
|
103
|
+
properties[name] = {
|
|
104
|
+
"type": spec.get("type", "string"),
|
|
105
|
+
"description": spec.get("description", ""),
|
|
106
|
+
}
|
|
107
|
+
if spec.get("enum"):
|
|
108
|
+
properties[name]["enum"] = spec["enum"]
|
|
109
|
+
if spec.get("required"):
|
|
110
|
+
required.append(name)
|
|
111
|
+
return {
|
|
112
|
+
"type": "function",
|
|
113
|
+
"function": {
|
|
114
|
+
"name": self.id,
|
|
115
|
+
"description": self.description_for_ai,
|
|
116
|
+
"parameters": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"properties": properties,
|
|
119
|
+
"required": required,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class Manifest:
|
|
127
|
+
service: Service
|
|
128
|
+
auth: Auth
|
|
129
|
+
capabilities: list[Capability]
|
|
130
|
+
agent_hints: dict[str, str] = field(default_factory=dict)
|
|
131
|
+
apia_version: str = "1.0"
|
|
132
|
+
raw: dict[str, Any] = field(default_factory=dict, repr=False)
|
|
133
|
+
|
|
134
|
+
# Convenience aliases
|
|
135
|
+
@property
|
|
136
|
+
def id(self) -> str:
|
|
137
|
+
return self.service.id
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def name(self) -> str:
|
|
141
|
+
return self.service.name
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def category(self) -> str:
|
|
145
|
+
return self.service.category
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def geo(self) -> list[str]:
|
|
149
|
+
return self.service.geo
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def is_free(self) -> bool:
|
|
153
|
+
return self.auth.anonymous_access
|
|
154
|
+
|
|
155
|
+
def find_capability(self, task: str) -> Capability | None:
|
|
156
|
+
"""Return the first capability whose intent matches the task."""
|
|
157
|
+
for cap in self.capabilities:
|
|
158
|
+
if cap.matches_intent(task):
|
|
159
|
+
return cap
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
def to_system_prompt(self) -> str:
|
|
163
|
+
"""Format this manifest as a system prompt section for an LLM."""
|
|
164
|
+
lines = [
|
|
165
|
+
f"## {self.service.name}",
|
|
166
|
+
f"{self.service.description_for_ai}",
|
|
167
|
+
f"Auth: {self.auth.type} | Cost: {self.auth.cost}",
|
|
168
|
+
f"Docs: {self.service.docs}",
|
|
169
|
+
"",
|
|
170
|
+
"### Capabilities",
|
|
171
|
+
]
|
|
172
|
+
for cap in self.capabilities:
|
|
173
|
+
lines.append(f"**[{cap.id}]** `{cap.endpoint}`")
|
|
174
|
+
lines.append(f"When: {cap.description_for_ai}")
|
|
175
|
+
lines.append(f"Intent: {', '.join(cap.intent[:5])}")
|
|
176
|
+
lines.append("")
|
|
177
|
+
if self.agent_hints:
|
|
178
|
+
lines.append("### Hints")
|
|
179
|
+
for k, v in self.agent_hints.items():
|
|
180
|
+
lines.append(f"- **{k}**: {v}")
|
|
181
|
+
return "\n".join(lines)
|
|
182
|
+
|
|
183
|
+
def to_openai_tools(self) -> list[dict[str, Any]]:
|
|
184
|
+
"""Convert all capabilities to OpenAI tool definitions."""
|
|
185
|
+
return [cap.to_openai_tool() for cap in self.capabilities]
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def from_dict(cls, data: dict[str, Any]) -> "Manifest":
|
|
189
|
+
return cls(
|
|
190
|
+
service=Service.from_dict(data.get("service", {})),
|
|
191
|
+
auth=Auth.from_dict(data.get("auth", {})),
|
|
192
|
+
capabilities=[Capability.from_dict(c) for c in data.get("capabilities", [])],
|
|
193
|
+
agent_hints=data.get("agent_hints", {}),
|
|
194
|
+
apia_version=data.get("apia", "1.0"),
|
|
195
|
+
raw=data,
|
|
196
|
+
)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
APIA Registry — loads and searches the manifest index.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .manifest import Manifest
|
|
11
|
+
from .exceptions import RegistryError, ManifestNotFoundError
|
|
12
|
+
|
|
13
|
+
REGISTRY_URL = (
|
|
14
|
+
"https://raw.githubusercontent.com/Komsomol39/apia-standard/main/registry.json"
|
|
15
|
+
)
|
|
16
|
+
MANIFEST_BASE_URL = (
|
|
17
|
+
"https://raw.githubusercontent.com/Komsomol39/apia-standard/main/manifests"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Registry:
|
|
22
|
+
"""
|
|
23
|
+
APIA manifest registry.
|
|
24
|
+
|
|
25
|
+
Usage::
|
|
26
|
+
|
|
27
|
+
registry = Registry()
|
|
28
|
+
apis = registry.find("send telegram message")
|
|
29
|
+
manifest = registry.get("telegram-bot")
|
|
30
|
+
tools = manifest.to_openai_tools()
|
|
31
|
+
prompt = manifest.to_system_prompt()
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, registry_url: str = REGISTRY_URL) -> None:
|
|
35
|
+
self._url = registry_url
|
|
36
|
+
self._index: list[dict[str, Any]] = []
|
|
37
|
+
self._cache: dict[str, Manifest] = {}
|
|
38
|
+
|
|
39
|
+
def _ensure_loaded(self) -> None:
|
|
40
|
+
if self._index:
|
|
41
|
+
return
|
|
42
|
+
try:
|
|
43
|
+
response = httpx.get(self._url, timeout=15.0)
|
|
44
|
+
response.raise_for_status()
|
|
45
|
+
data = response.json()
|
|
46
|
+
self._index = data.get("manifests", [])
|
|
47
|
+
except Exception as exc:
|
|
48
|
+
raise RegistryError(f"Failed to load APIA registry: {exc}") from exc
|
|
49
|
+
|
|
50
|
+
def list(
|
|
51
|
+
self,
|
|
52
|
+
category: str | None = None,
|
|
53
|
+
geo: str | None = None,
|
|
54
|
+
free_only: bool = False,
|
|
55
|
+
language: str | None = None,
|
|
56
|
+
) -> list[dict[str, Any]]:
|
|
57
|
+
"""
|
|
58
|
+
List registry entries with optional filters.
|
|
59
|
+
|
|
60
|
+
:param category: One of the 26 APIA categories e.g. "ai", "finance", "maps".
|
|
61
|
+
:param geo: ISO country code or "GLOBAL" e.g. "RU", "US", "GLOBAL".
|
|
62
|
+
:param free_only: Only return APIs with anonymous_access=True.
|
|
63
|
+
:param language: Primary language e.g. "ru", "en".
|
|
64
|
+
:returns: List of lightweight registry entries (not full manifests).
|
|
65
|
+
"""
|
|
66
|
+
self._ensure_loaded()
|
|
67
|
+
results = self._index
|
|
68
|
+
if category:
|
|
69
|
+
results = [m for m in results if m.get("category") == category]
|
|
70
|
+
if geo:
|
|
71
|
+
results = [m for m in results if geo in m.get("geo", []) or "GLOBAL" in m.get("geo", [])]
|
|
72
|
+
if free_only:
|
|
73
|
+
results = [m for m in results if m.get("anonymous_access")]
|
|
74
|
+
if language:
|
|
75
|
+
results = [m for m in results if m.get("language") == language]
|
|
76
|
+
return results
|
|
77
|
+
|
|
78
|
+
def categories(self) -> dict[str, int]:
|
|
79
|
+
"""Return a dict of {category: count} for all manifests."""
|
|
80
|
+
self._ensure_loaded()
|
|
81
|
+
from collections import Counter
|
|
82
|
+
return dict(Counter(m.get("category", "") for m in self._index))
|
|
83
|
+
|
|
84
|
+
def find(
|
|
85
|
+
self,
|
|
86
|
+
task: str,
|
|
87
|
+
category: str | None = None,
|
|
88
|
+
geo: str | None = None,
|
|
89
|
+
top_k: int = 3,
|
|
90
|
+
) -> list[Manifest]:
|
|
91
|
+
"""
|
|
92
|
+
Find the most relevant APIs for a natural language task.
|
|
93
|
+
|
|
94
|
+
Searches intent phrases in capabilities. Returns full Manifest objects.
|
|
95
|
+
|
|
96
|
+
:param task: Natural language description e.g. "send a telegram message".
|
|
97
|
+
:param category: Narrow to a specific category.
|
|
98
|
+
:param geo: Narrow to a specific geography.
|
|
99
|
+
:param top_k: Maximum number of results.
|
|
100
|
+
:returns: List of Manifest objects sorted by relevance.
|
|
101
|
+
"""
|
|
102
|
+
self._ensure_loaded()
|
|
103
|
+
task_lower = task.lower()
|
|
104
|
+
scored: list[tuple[int, dict[str, Any]]] = []
|
|
105
|
+
|
|
106
|
+
candidates = self._index
|
|
107
|
+
if category:
|
|
108
|
+
candidates = [m for m in candidates if m.get("category") == category]
|
|
109
|
+
if geo:
|
|
110
|
+
candidates = [m for m in candidates if geo in m.get("geo", []) or "GLOBAL" in m.get("geo", [])]
|
|
111
|
+
|
|
112
|
+
for entry in candidates:
|
|
113
|
+
score = 0
|
|
114
|
+
# Match description
|
|
115
|
+
if any(w in entry.get("description_for_ai", "").lower() for w in task_lower.split()):
|
|
116
|
+
score += 1
|
|
117
|
+
# Match capability intents (stronger signal)
|
|
118
|
+
for cap in entry.get("capabilities", []):
|
|
119
|
+
for intent in cap.get("intent", []):
|
|
120
|
+
if task_lower in intent.lower() or intent.lower() in task_lower:
|
|
121
|
+
score += 3
|
|
122
|
+
break
|
|
123
|
+
if score > 0:
|
|
124
|
+
scored.append((score, entry))
|
|
125
|
+
|
|
126
|
+
scored.sort(key=lambda x: -x[0])
|
|
127
|
+
top_entries = [e for _, e in scored[:top_k]]
|
|
128
|
+
|
|
129
|
+
# Load full manifests for top results
|
|
130
|
+
return [self.get(e["id"]) for e in top_entries]
|
|
131
|
+
|
|
132
|
+
def get(self, api_id: str) -> Manifest:
|
|
133
|
+
"""
|
|
134
|
+
Load a full manifest by API id.
|
|
135
|
+
|
|
136
|
+
:param api_id: The manifest id e.g. "openai", "telegram-bot", "stripe".
|
|
137
|
+
:raises ManifestNotFoundError: If the manifest cannot be found.
|
|
138
|
+
:returns: Full Manifest object.
|
|
139
|
+
"""
|
|
140
|
+
if api_id in self._cache:
|
|
141
|
+
return self._cache[api_id]
|
|
142
|
+
url = f"{MANIFEST_BASE_URL}/{api_id}/apia.json"
|
|
143
|
+
try:
|
|
144
|
+
response = httpx.get(url, timeout=10.0)
|
|
145
|
+
if response.status_code == 404:
|
|
146
|
+
raise ManifestNotFoundError(f"Manifest not found: {api_id!r}")
|
|
147
|
+
response.raise_for_status()
|
|
148
|
+
manifest = Manifest.from_dict(response.json())
|
|
149
|
+
self._cache[api_id] = manifest
|
|
150
|
+
return manifest
|
|
151
|
+
except ManifestNotFoundError:
|
|
152
|
+
raise
|
|
153
|
+
except Exception as exc:
|
|
154
|
+
raise RegistryError(f"Failed to load manifest {api_id!r}: {exc}") from exc
|
|
155
|
+
|
|
156
|
+
def build_system_prompt(
|
|
157
|
+
self,
|
|
158
|
+
apis: list[Manifest],
|
|
159
|
+
header: str = "You are an AI agent with access to the following APIs:",
|
|
160
|
+
) -> str:
|
|
161
|
+
"""
|
|
162
|
+
Build a system prompt containing multiple API manifests.
|
|
163
|
+
|
|
164
|
+
:param apis: List of Manifest objects to include.
|
|
165
|
+
:param header: Opening sentence for the system prompt.
|
|
166
|
+
:returns: Complete system prompt string ready to pass to an LLM.
|
|
167
|
+
"""
|
|
168
|
+
parts = [header, ""]
|
|
169
|
+
for manifest in apis:
|
|
170
|
+
parts.append(manifest.to_system_prompt())
|
|
171
|
+
parts.append("---")
|
|
172
|
+
parts.append(
|
|
173
|
+
"\nWhen the user asks something, identify which API and capability to use, "
|
|
174
|
+
"explain your reasoning, and provide the exact API call."
|
|
175
|
+
)
|
|
176
|
+
return "\n".join(parts)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "apia"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for APIA — AI-native API manifests standard"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "APIA Community" }]
|
|
12
|
+
keywords = ["apia", "api", "ai", "agents", "llm", "manifests"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Software Development :: Libraries",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
|
+
]
|
|
25
|
+
requires-python = ">=3.9"
|
|
26
|
+
dependencies = [
|
|
27
|
+
"httpx>=0.24.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = ["pytest", "pytest-asyncio", "ruff", "mypy"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/Komsomol39/apia-standard"
|
|
35
|
+
Repository = "https://github.com/Komsomol39/apia-py"
|
|
36
|
+
Issues = "https://github.com/Komsomol39/apia-py/issues"
|
|
37
|
+
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
line-length = 100
|
|
40
|
+
target-version = "py39"
|
|
41
|
+
|
|
42
|
+
[tool.mypy]
|
|
43
|
+
python_version = "3.9"
|
|
44
|
+
strict = true
|
apia-0.1.0/pytest.ini
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Tests for apia Python SDK."""
|
|
2
|
+
import pytest
|
|
3
|
+
from unittest.mock import patch, MagicMock
|
|
4
|
+
from apia import Registry, Manifest, Capability, Service, Auth
|
|
5
|
+
from apia.exceptions import ManifestNotFoundError, RegistryError
|
|
6
|
+
|
|
7
|
+
# ── Fixtures ────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
SAMPLE_MANIFEST = {
|
|
10
|
+
"apia": "1.0",
|
|
11
|
+
"service": {
|
|
12
|
+
"id": "telegram-bot",
|
|
13
|
+
"name": "Telegram Bot API",
|
|
14
|
+
"description_for_ai": "Send messages via Telegram. 950M users.",
|
|
15
|
+
"category": "social",
|
|
16
|
+
"geo": ["GLOBAL"],
|
|
17
|
+
"language": "en",
|
|
18
|
+
"url": "https://telegram.org",
|
|
19
|
+
"api_base": "https://api.telegram.org/bot{token}",
|
|
20
|
+
"docs": "https://core.telegram.org/bots/api",
|
|
21
|
+
},
|
|
22
|
+
"auth": {"type": "apikey", "anonymous_access": False, "cost": "Free"},
|
|
23
|
+
"capabilities": [
|
|
24
|
+
{
|
|
25
|
+
"id": "send_message",
|
|
26
|
+
"description_for_ai": "Send a text message to a user or group.",
|
|
27
|
+
"intent": ["send telegram message", "telegram notify", "telegram bot send"],
|
|
28
|
+
"endpoint": "POST https://api.telegram.org/bot{token}/sendMessage",
|
|
29
|
+
"input": {
|
|
30
|
+
"chat_id": {"type": "string", "required": True, "description": "Telegram chat ID"},
|
|
31
|
+
"text": {"type": "string", "required": True, "description": "Message text"},
|
|
32
|
+
"parse_mode": {"type": "string", "required": False, "description": "HTML or Markdown"},
|
|
33
|
+
},
|
|
34
|
+
"output": {"type": "message", "fields": ["message_id", "chat.id", "text"]},
|
|
35
|
+
"realtime": True,
|
|
36
|
+
"requires_auth": True,
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"agent_hints": {"bot_father": "Get token from @BotFather on Telegram"},
|
|
40
|
+
"meta": {"apia_version": "1.0", "last_verified": "2026-06-14"},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
SAMPLE_REGISTRY = {
|
|
44
|
+
"_meta": {"total": 1, "generated": "2026-06-14"},
|
|
45
|
+
"manifests": [
|
|
46
|
+
{
|
|
47
|
+
"id": "telegram-bot",
|
|
48
|
+
"name": "Telegram Bot API",
|
|
49
|
+
"description_for_ai": "Send messages via Telegram. 950M users.",
|
|
50
|
+
"category": "social",
|
|
51
|
+
"geo": ["GLOBAL"],
|
|
52
|
+
"language": "en",
|
|
53
|
+
"auth_type": "apikey",
|
|
54
|
+
"anonymous_access": False,
|
|
55
|
+
"cost": "Free",
|
|
56
|
+
"capabilities": [
|
|
57
|
+
{
|
|
58
|
+
"id": "send_message",
|
|
59
|
+
"description_for_ai": "Send a text message.",
|
|
60
|
+
"intent": ["send telegram message", "telegram notify"],
|
|
61
|
+
"endpoint": "POST https://api.telegram.org/bot{token}/sendMessage",
|
|
62
|
+
"realtime": True,
|
|
63
|
+
"requires_auth": True,
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"manifest_url": "https://raw.githubusercontent.com/Komsomol39/apia-standard/main/manifests/telegram-bot/apia.json",
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ── Manifest tests ───────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
class TestManifest:
|
|
75
|
+
def test_from_dict(self):
|
|
76
|
+
m = Manifest.from_dict(SAMPLE_MANIFEST)
|
|
77
|
+
assert m.id == "telegram-bot"
|
|
78
|
+
assert m.name == "Telegram Bot API"
|
|
79
|
+
assert m.category == "social"
|
|
80
|
+
assert m.is_free is False
|
|
81
|
+
assert len(m.capabilities) == 1
|
|
82
|
+
|
|
83
|
+
def test_find_capability_match(self):
|
|
84
|
+
m = Manifest.from_dict(SAMPLE_MANIFEST)
|
|
85
|
+
cap = m.find_capability("send telegram message")
|
|
86
|
+
assert cap is not None
|
|
87
|
+
assert cap.id == "send_message"
|
|
88
|
+
|
|
89
|
+
def test_find_capability_no_match(self):
|
|
90
|
+
m = Manifest.from_dict(SAMPLE_MANIFEST)
|
|
91
|
+
cap = m.find_capability("book a hotel room")
|
|
92
|
+
assert cap is None
|
|
93
|
+
|
|
94
|
+
def test_to_openai_tools(self):
|
|
95
|
+
m = Manifest.from_dict(SAMPLE_MANIFEST)
|
|
96
|
+
tools = m.to_openai_tools()
|
|
97
|
+
assert len(tools) == 1
|
|
98
|
+
tool = tools[0]
|
|
99
|
+
assert tool["type"] == "function"
|
|
100
|
+
assert tool["function"]["name"] == "send_message"
|
|
101
|
+
assert "chat_id" in tool["function"]["parameters"]["properties"]
|
|
102
|
+
assert "chat_id" in tool["function"]["parameters"]["required"]
|
|
103
|
+
assert "parse_mode" not in tool["function"]["parameters"]["required"]
|
|
104
|
+
|
|
105
|
+
def test_to_system_prompt(self):
|
|
106
|
+
m = Manifest.from_dict(SAMPLE_MANIFEST)
|
|
107
|
+
prompt = m.to_system_prompt()
|
|
108
|
+
assert "Telegram Bot API" in prompt
|
|
109
|
+
assert "send_message" in prompt
|
|
110
|
+
assert "POST" in prompt
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ── Capability tests ─────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
class TestCapability:
|
|
116
|
+
def test_matches_intent(self):
|
|
117
|
+
cap = Capability.from_dict(SAMPLE_MANIFEST["capabilities"][0])
|
|
118
|
+
assert cap.matches_intent("send telegram message") is True
|
|
119
|
+
assert cap.matches_intent("telegram notify") is True
|
|
120
|
+
assert cap.matches_intent("book flight") is False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ── Registry tests ───────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
class TestRegistry:
|
|
126
|
+
def _registry_with_mock_index(self):
|
|
127
|
+
registry = Registry()
|
|
128
|
+
registry._index = SAMPLE_REGISTRY["manifests"]
|
|
129
|
+
return registry
|
|
130
|
+
|
|
131
|
+
def test_list_all(self):
|
|
132
|
+
r = self._registry_with_mock_index()
|
|
133
|
+
entries = r.list()
|
|
134
|
+
assert len(entries) == 1
|
|
135
|
+
|
|
136
|
+
def test_list_by_category(self):
|
|
137
|
+
r = self._registry_with_mock_index()
|
|
138
|
+
assert len(r.list(category="social")) == 1
|
|
139
|
+
assert len(r.list(category="finance")) == 0
|
|
140
|
+
|
|
141
|
+
def test_list_by_geo(self):
|
|
142
|
+
r = self._registry_with_mock_index()
|
|
143
|
+
assert len(r.list(geo="RU")) == 1 # GLOBAL includes RU
|
|
144
|
+
assert len(r.list(geo="US")) == 1
|
|
145
|
+
|
|
146
|
+
def test_list_free_only(self):
|
|
147
|
+
r = self._registry_with_mock_index()
|
|
148
|
+
assert len(r.list(free_only=True)) == 0 # telegram-bot is not anonymous
|
|
149
|
+
|
|
150
|
+
def test_categories(self):
|
|
151
|
+
r = self._registry_with_mock_index()
|
|
152
|
+
cats = r.categories()
|
|
153
|
+
assert cats == {"social": 1}
|
|
154
|
+
|
|
155
|
+
def test_find_by_intent(self):
|
|
156
|
+
r = self._registry_with_mock_index()
|
|
157
|
+
with patch.object(r, "get", return_value=Manifest.from_dict(SAMPLE_MANIFEST)):
|
|
158
|
+
results = r.find("send telegram message")
|
|
159
|
+
assert len(results) == 1
|
|
160
|
+
assert results[0].id == "telegram-bot"
|
|
161
|
+
|
|
162
|
+
def test_find_no_results(self):
|
|
163
|
+
r = self._registry_with_mock_index()
|
|
164
|
+
results = r.find("xyzzy-nonexistent-quantum-blockchain")
|
|
165
|
+
assert results == []
|
|
166
|
+
|
|
167
|
+
def test_get_manifest(self):
|
|
168
|
+
r = self._registry_with_mock_index()
|
|
169
|
+
mock_response = MagicMock()
|
|
170
|
+
mock_response.status_code = 200
|
|
171
|
+
mock_response.json.return_value = SAMPLE_MANIFEST
|
|
172
|
+
with patch("httpx.get", return_value=mock_response):
|
|
173
|
+
m = r.get("telegram-bot")
|
|
174
|
+
assert m.id == "telegram-bot"
|
|
175
|
+
|
|
176
|
+
def test_get_not_found(self):
|
|
177
|
+
r = self._registry_with_mock_index()
|
|
178
|
+
mock_response = MagicMock()
|
|
179
|
+
mock_response.status_code = 404
|
|
180
|
+
with patch("httpx.get", return_value=mock_response):
|
|
181
|
+
with pytest.raises(ManifestNotFoundError):
|
|
182
|
+
r.get("nonexistent-api")
|
|
183
|
+
|
|
184
|
+
def test_get_cached(self):
|
|
185
|
+
r = self._registry_with_mock_index()
|
|
186
|
+
m = Manifest.from_dict(SAMPLE_MANIFEST)
|
|
187
|
+
r._cache["telegram-bot"] = m
|
|
188
|
+
with patch("httpx.get") as mock_get:
|
|
189
|
+
result = r.get("telegram-bot")
|
|
190
|
+
mock_get.assert_not_called()
|
|
191
|
+
assert result is m
|
|
192
|
+
|
|
193
|
+
def test_build_system_prompt(self):
|
|
194
|
+
r = self._registry_with_mock_index()
|
|
195
|
+
m = Manifest.from_dict(SAMPLE_MANIFEST)
|
|
196
|
+
prompt = r.build_system_prompt([m])
|
|
197
|
+
assert "You are an AI agent" in prompt
|
|
198
|
+
assert "Telegram Bot API" in prompt
|