apia 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -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
+ ]
apia/exceptions.py ADDED
@@ -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."""
apia/manifest.py ADDED
@@ -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
+ )
apia/registry.py ADDED
@@ -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,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
@@ -0,0 +1,7 @@
1
+ apia/__init__.py,sha256=-aC12FBNJ8tjje_W6hobrHfVsgIgIfDZCnUQxcX3BFM,623
2
+ apia/exceptions.py,sha256=Xp2pnG93NtagVViz0lDLQZcTjBjvVG0g7TjYPxc3BIw,320
3
+ apia/manifest.py,sha256=HUqoTR_kPXddA3BnpbumBL6bGC4-brgWhM9Zm7mIydU,6176
4
+ apia/registry.py,sha256=6ZCJlZrjZIW9XLdi2wAkAxPn3d6KhFa-xI8iL9qzRgc,6324
5
+ apia-0.1.0.dist-info/METADATA,sha256=1votxv_t7bbArMALlTfkpPXKAz-DlpUbEsi8Y22z_GQ,4231
6
+ apia-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ apia-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any