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.
@@ -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
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ .pytest_cache/
10
+ *.egg
11
+ MANIFEST
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,3 @@
1
+ [pytest]
2
+ testpaths = tests
3
+ asyncio_mode = auto
@@ -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