agentified 0.0.3__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,43 @@
1
+ # Rust
2
+ target/
3
+ **/*.rs.bk
4
+
5
+ # Environment
6
+ .env
7
+ .env.local
8
+ .env.*.local
9
+
10
+ # IDE
11
+ .idea/
12
+ .vscode/
13
+ *.swp
14
+ *.swo
15
+
16
+ # OS
17
+ .DS_Store
18
+ Thumbs.db
19
+
20
+ # Node
21
+ node_modules/
22
+
23
+ # Python
24
+ __pycache__/
25
+ *.py[cod]
26
+ *.egg-info/
27
+ *.egg
28
+ dist/
29
+ build/
30
+ .pytest_cache/
31
+ .mypy_cache/
32
+ .ruff_cache/
33
+ *.whl
34
+
35
+ # TypeScript
36
+ *.tsbuildinfo
37
+ ts-packages/**/src/**/*.js
38
+ ts-packages/**/src/**/*.js.map
39
+ ts-packages/**/src/**/*.d.ts
40
+ ts-packages/**/src/**/*.d.ts.map
41
+
42
+ # Logs
43
+ *.log
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentified
3
+ Version: 0.0.3
4
+ Summary: Python SDK for Agentified — Context Intelligence Layer for AI agents
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx<1,>=0.27
7
+ Requires-Dist: pydantic<3,>=2.0
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
10
+ Requires-Dist: pytest>=8; extra == 'dev'
11
+ Requires-Dist: respx>=0.22; extra == 'dev'
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agentified"
7
+ version = "0.0.3"
8
+ description = "Python SDK for Agentified — Context Intelligence Layer for AI agents"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "httpx>=0.27,<1",
12
+ "pydantic>=2.0,<3",
13
+ ]
14
+
15
+ [project.optional-dependencies]
16
+ dev = [
17
+ "pytest>=8",
18
+ "pytest-asyncio>=0.24",
19
+ "respx>=0.22",
20
+ ]
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/agentified"]
24
+
25
+ [tool.pytest.ini_options]
26
+ asyncio_mode = "auto"
@@ -0,0 +1,51 @@
1
+ from .client import Agentified
2
+ from ._sync import SyncAgentified
3
+ from .tool import tool
4
+ from .models import (
5
+ AgentifiedConfig,
6
+ AgentifiedEvent,
7
+ CaptureTurnOptions,
8
+ CaptureTurnResponse,
9
+ DiscoverCompleteEvent,
10
+ DiscoverResponse,
11
+ DiscoverStartEvent,
12
+ DiscoverTool,
13
+ DiscoverToolInput,
14
+ Message,
15
+ PrefetchCompleteEvent,
16
+ PrefetchOptions,
17
+ PrefetchSkippedEvent,
18
+ PrefetchStartEvent,
19
+ RankedTool,
20
+ RegisterResponse,
21
+ ServerTool,
22
+ ServerToolFields,
23
+ TokenUsage,
24
+ ToolDefinition,
25
+ )
26
+
27
+ __all__ = [
28
+ "Agentified",
29
+ "SyncAgentified",
30
+ "tool",
31
+ "AgentifiedConfig",
32
+ "AgentifiedEvent",
33
+ "CaptureTurnOptions",
34
+ "CaptureTurnResponse",
35
+ "DiscoverCompleteEvent",
36
+ "DiscoverResponse",
37
+ "DiscoverStartEvent",
38
+ "DiscoverTool",
39
+ "DiscoverToolInput",
40
+ "Message",
41
+ "PrefetchCompleteEvent",
42
+ "PrefetchOptions",
43
+ "PrefetchSkippedEvent",
44
+ "PrefetchStartEvent",
45
+ "RankedTool",
46
+ "RegisterResponse",
47
+ "ServerTool",
48
+ "ServerToolFields",
49
+ "TokenUsage",
50
+ "ToolDefinition",
51
+ ]
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ from .client import Agentified
7
+ from .models import (
8
+ AgentifiedConfig,
9
+ CaptureTurnResponse,
10
+ DiscoverTool,
11
+ RankedTool,
12
+ RegisterResponse,
13
+ ServerTool,
14
+ )
15
+
16
+
17
+ class SyncAgentified:
18
+ def __init__(self, config: AgentifiedConfig) -> None:
19
+ self._async = Agentified(config)
20
+
21
+ def register(self) -> RegisterResponse:
22
+ return asyncio.run(self._async.register())
23
+
24
+ def prefetch(self, **kwargs: Any) -> list[RankedTool]:
25
+ return asyncio.run(self._async.prefetch(**kwargs))
26
+
27
+ def capture_turn(self, **kwargs: Any) -> CaptureTurnResponse:
28
+ return asyncio.run(self._async.capture_turn(**kwargs))
29
+
30
+ def get_frontend_tools(self) -> list[ServerTool]:
31
+ return self._async.get_frontend_tools()
32
+
33
+ def get_frontend_tool_names(self) -> list[str]:
34
+ return self._async.get_frontend_tool_names()
35
+
36
+ def as_discover_tool(self) -> DiscoverTool:
37
+ return self._async.as_discover_tool()
@@ -0,0 +1,156 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+ from .models import (
9
+ AgentifiedConfig,
10
+ AgentifiedEvent,
11
+ CaptureTurnResponse,
12
+ DiscoverResponse,
13
+ DiscoverStartEvent,
14
+ DiscoverCompleteEvent,
15
+ DiscoverTool,
16
+ DiscoverToolInput,
17
+ PrefetchCompleteEvent,
18
+ PrefetchStartEvent,
19
+ RankedTool,
20
+ RegisterResponse,
21
+ Message,
22
+ ServerTool,
23
+ ToolDefinition,
24
+ )
25
+
26
+
27
+ class Agentified:
28
+ def __init__(self, config: AgentifiedConfig) -> None:
29
+ self._config = config
30
+ self._client: httpx.AsyncClient | None = None
31
+
32
+ async def __aenter__(self) -> Agentified:
33
+ self._client = httpx.AsyncClient()
34
+ return self
35
+
36
+ async def __aexit__(self, *_: Any) -> None:
37
+ if self._client:
38
+ await self._client.aclose()
39
+ self._client = None
40
+
41
+ async def register(self) -> RegisterResponse:
42
+ data = {"tools": [t.model_dump(exclude_none=True) for t in self._config.tools]}
43
+ resp = await self._http_client.post(
44
+ f"{self._config.server_url}/api/v1/tools", json=data
45
+ )
46
+ return RegisterResponse.model_validate(resp.json())
47
+
48
+ async def prefetch(
49
+ self,
50
+ *,
51
+ messages: list[dict[str, str]],
52
+ limit: int | None = None,
53
+ exclude: list[str] | None = None,
54
+ turn_id: str | None = None,
55
+ ) -> list[RankedTool]:
56
+ msg_models = [Message(**m) for m in messages]
57
+ self._emit(PrefetchStartEvent(messages=msg_models))
58
+ start = time.perf_counter()
59
+
60
+ query = "\n".join(m["content"] for m in messages)
61
+ tools = await self._discover(query, limit, exclude, turn_id)
62
+
63
+ duration_ms = (time.perf_counter() - start) * 1000
64
+ self._emit(PrefetchCompleteEvent(tools=tools, duration_ms=duration_ms))
65
+ return tools
66
+
67
+ async def capture_turn(
68
+ self, *, tools_loaded: list[str], message: str
69
+ ) -> CaptureTurnResponse:
70
+ resp = await self._http_client.post(
71
+ f"{self._config.server_url}/api/v1/turns",
72
+ json={"tools_loaded": tools_loaded, "message": message},
73
+ )
74
+ return CaptureTurnResponse.model_validate(resp.json())
75
+
76
+ def get_frontend_tools(self) -> list[ServerTool]:
77
+ return [
78
+ t
79
+ for t in self._config.tools
80
+ if t.metadata and t.metadata.get("location") == "frontend"
81
+ ]
82
+
83
+ def get_frontend_tool_names(self) -> list[str]:
84
+ return [t.name for t in self.get_frontend_tools()]
85
+
86
+ def as_discover_tool(self) -> DiscoverTool:
87
+ async def execute(input: dict[str, Any] | DiscoverToolInput) -> list[RankedTool]:
88
+ if isinstance(input, dict):
89
+ input = DiscoverToolInput(**input)
90
+ self._emit(DiscoverStartEvent(query=input.query))
91
+ start = time.perf_counter()
92
+
93
+ tools = await self._discover(input.query, input.limit)
94
+
95
+ duration_ms = (time.perf_counter() - start) * 1000
96
+ self._emit(
97
+ DiscoverCompleteEvent(
98
+ query=input.query, tools=tools, duration_ms=duration_ms
99
+ )
100
+ )
101
+ return tools
102
+
103
+ return DiscoverTool(
104
+ definition=ToolDefinition(
105
+ name="agentified_discover",
106
+ description="Find tools relevant to the current task. Call this when you need capabilities you don't have.",
107
+ parameters={
108
+ "type": "object",
109
+ "properties": {
110
+ "query": {
111
+ "type": "string",
112
+ "description": "Natural language description of what you need to do",
113
+ },
114
+ "limit": {
115
+ "type": "number",
116
+ "description": "Max number of tools to return",
117
+ },
118
+ },
119
+ "required": ["query"],
120
+ },
121
+ ),
122
+ execute=execute,
123
+ )
124
+
125
+ # -- private --
126
+
127
+ @property
128
+ def _http_client(self) -> httpx.AsyncClient:
129
+ if self._client is None:
130
+ self._client = httpx.AsyncClient()
131
+ return self._client
132
+
133
+ async def _discover(
134
+ self,
135
+ query: str,
136
+ limit: int | None = None,
137
+ exclude: list[str] | None = None,
138
+ turn_id: str | None = None,
139
+ ) -> list[RankedTool]:
140
+ body: dict[str, Any] = {"query": query}
141
+ if limit is not None:
142
+ body["limit"] = limit
143
+ if exclude is not None:
144
+ body["exclude"] = exclude
145
+ if turn_id is not None:
146
+ body["turn_id"] = turn_id
147
+
148
+ resp = await self._http_client.post(
149
+ f"{self._config.server_url}/api/v1/discover", json=body
150
+ )
151
+ data = DiscoverResponse.model_validate(resp.json())
152
+ return data.tools
153
+
154
+ def _emit(self, event: AgentifiedEvent) -> None:
155
+ if self._config.on_event:
156
+ self._config.on_event(event)
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Awaitable, Callable, Literal, Union
5
+
6
+ from pydantic import BaseModel
7
+
8
+
9
+ # Wire-format models (match Rust server snake_case JSON)
10
+
11
+ class ServerToolFields(BaseModel):
12
+ name: str
13
+ description: str
14
+ input_schema: str | None = None
15
+ output_schema: str | None = None
16
+
17
+
18
+ class ServerTool(BaseModel):
19
+ name: str
20
+ description: str
21
+ parameters: dict[str, Any]
22
+ metadata: dict[str, Any] | None = None
23
+ fields: ServerToolFields | None = None
24
+
25
+
26
+ class RankedTool(ServerTool):
27
+ score: float
28
+ graph_expanded: bool | None = None
29
+
30
+
31
+ class ToolDefinition(BaseModel):
32
+ name: str
33
+ description: str
34
+ parameters: dict[str, Any]
35
+ metadata: dict[str, Any] | None = None
36
+
37
+
38
+ class RegisterResponse(BaseModel):
39
+ registered: int
40
+
41
+
42
+ class DiscoverResponse(BaseModel):
43
+ tools: list[RankedTool]
44
+
45
+
46
+ class Message(BaseModel):
47
+ role: str
48
+ content: str
49
+
50
+
51
+ class PrefetchOptions(BaseModel):
52
+ messages: list[Message]
53
+ limit: int | None = None
54
+ exclude: list[str] | None = None
55
+ turn_id: str | None = None
56
+
57
+
58
+ class CaptureTurnOptions(BaseModel):
59
+ tools_loaded: list[str]
60
+ message: str
61
+
62
+
63
+ class CaptureTurnResponse(BaseModel):
64
+ turn_id: str
65
+
66
+
67
+ class DiscoverToolInput(BaseModel):
68
+ query: str
69
+ limit: int | None = None
70
+
71
+
72
+ class TokenUsage(BaseModel):
73
+ input: int
74
+ output: int
75
+ cached: int
76
+ reasoning: int
77
+
78
+
79
+ # Event types
80
+
81
+ class PrefetchStartEvent(BaseModel):
82
+ type: Literal["agentified:prefetch:start"] = "agentified:prefetch:start"
83
+ messages: list[Message]
84
+
85
+
86
+ class PrefetchCompleteEvent(BaseModel):
87
+ type: Literal["agentified:prefetch:complete"] = "agentified:prefetch:complete"
88
+ tools: list[RankedTool]
89
+ duration_ms: float
90
+ token_usage: TokenUsage | None = None
91
+
92
+
93
+ class PrefetchSkippedEvent(BaseModel):
94
+ type: Literal["agentified:prefetch:skipped"] = "agentified:prefetch:skipped"
95
+ tools: list[RankedTool]
96
+ duration_ms: float
97
+
98
+
99
+ class DiscoverStartEvent(BaseModel):
100
+ type: Literal["agentified:discover:start"] = "agentified:discover:start"
101
+ query: str
102
+
103
+
104
+ class DiscoverCompleteEvent(BaseModel):
105
+ type: Literal["agentified:discover:complete"] = "agentified:discover:complete"
106
+ query: str
107
+ tools: list[RankedTool]
108
+ duration_ms: float
109
+ token_usage: TokenUsage | None = None
110
+
111
+
112
+ AgentifiedEvent = Union[
113
+ PrefetchStartEvent,
114
+ PrefetchCompleteEvent,
115
+ PrefetchSkippedEvent,
116
+ DiscoverStartEvent,
117
+ DiscoverCompleteEvent,
118
+ ]
119
+
120
+
121
+ # Non-serializable containers (use dataclasses for callables)
122
+
123
+ @dataclass
124
+ class DiscoverTool:
125
+ definition: ToolDefinition
126
+ execute: Callable[[DiscoverToolInput], Awaitable[list[RankedTool]]]
127
+
128
+
129
+ @dataclass
130
+ class AgentifiedConfig:
131
+ server_url: str
132
+ tools: list[ServerTool]
133
+ on_event: Callable[[AgentifiedEvent], None] | None = field(default=None)
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from .models import ServerTool, ServerToolFields
7
+
8
+
9
+ def tool(
10
+ *,
11
+ name: str,
12
+ description: str,
13
+ parameters: dict[str, Any],
14
+ metadata: dict[str, Any] | None = None,
15
+ ) -> ServerTool:
16
+ return ServerTool(
17
+ name=name,
18
+ description=description,
19
+ parameters=parameters,
20
+ **({"metadata": metadata} if metadata else {}),
21
+ fields=ServerToolFields(
22
+ name=name,
23
+ description=description,
24
+ input_schema=json.dumps(parameters),
25
+ ),
26
+ )
@@ -0,0 +1,14 @@
1
+ from agentified.models import RankedTool, ServerTool
2
+
3
+ TEST_URL = "http://localhost:9119"
4
+
5
+ TEST_TOOL = ServerTool(
6
+ name="get_weather",
7
+ description="Get weather for a city",
8
+ parameters={"type": "object", "properties": {"city": {"type": "string"}}},
9
+ )
10
+
11
+ RANKED_TOOL = RankedTool(
12
+ **TEST_TOOL.model_dump(),
13
+ score=0.95,
14
+ )
@@ -0,0 +1,293 @@
1
+ import httpx
2
+ import pytest
3
+ import respx
4
+
5
+ from agentified.client import Agentified
6
+ from agentified.models import (
7
+ AgentifiedConfig,
8
+ AgentifiedEvent,
9
+ RankedTool,
10
+ ServerTool,
11
+ )
12
+
13
+ TEST_URL = "http://localhost:9119"
14
+
15
+ TEST_TOOL = ServerTool(
16
+ name="get_weather",
17
+ description="Get weather for a city",
18
+ parameters={"type": "object", "properties": {"city": {"type": "string"}}},
19
+ )
20
+
21
+ RANKED_TOOL = RankedTool(**TEST_TOOL.model_dump(), score=0.95)
22
+
23
+
24
+ class TestRegister:
25
+ @respx.mock
26
+ async def test_posts_tools_and_returns_registered_count(self):
27
+ route = respx.post(f"{TEST_URL}/api/v1/tools").mock(
28
+ return_value=httpx.Response(200, json={"registered": 1})
29
+ )
30
+
31
+ async with Agentified(
32
+ AgentifiedConfig(server_url=TEST_URL, tools=[TEST_TOOL])
33
+ ) as agent:
34
+ result = await agent.register()
35
+
36
+ assert result.registered == 1
37
+ assert route.called
38
+ body = route.calls[0].request.content
39
+ assert b'"tools"' in body
40
+
41
+
42
+ class TestPrefetch:
43
+ @respx.mock
44
+ async def test_posts_discover_from_messages_and_returns_ranked_tools(self):
45
+ respx.post(f"{TEST_URL}/api/v1/discover").mock(
46
+ return_value=httpx.Response(
47
+ 200, json={"tools": [RANKED_TOOL.model_dump()]}
48
+ )
49
+ )
50
+
51
+ async with Agentified(
52
+ AgentifiedConfig(server_url=TEST_URL, tools=[TEST_TOOL])
53
+ ) as agent:
54
+ result = await agent.prefetch(
55
+ messages=[{"role": "user", "content": "What is the weather in Paris?"}],
56
+ limit=5,
57
+ )
58
+
59
+ assert len(result) == 1
60
+ assert result[0].name == "get_weather"
61
+ assert result[0].score == 0.95
62
+
63
+ @respx.mock
64
+ async def test_emits_start_and_complete_events_with_timing(self):
65
+ respx.post(f"{TEST_URL}/api/v1/discover").mock(
66
+ return_value=httpx.Response(
67
+ 200, json={"tools": [RANKED_TOOL.model_dump()]}
68
+ )
69
+ )
70
+
71
+ events: list[AgentifiedEvent] = []
72
+ messages = [{"role": "user", "content": "weather in Paris"}]
73
+
74
+ async with Agentified(
75
+ AgentifiedConfig(
76
+ server_url=TEST_URL,
77
+ tools=[TEST_TOOL],
78
+ on_event=lambda e: events.append(e),
79
+ )
80
+ ) as agent:
81
+ await agent.prefetch(messages=messages)
82
+
83
+ assert len(events) == 2
84
+ assert events[0].type == "agentified:prefetch:start"
85
+ assert events[1].type == "agentified:prefetch:complete"
86
+ assert events[1].duration_ms >= 0
87
+
88
+ @respx.mock
89
+ async def test_passes_exclude_to_discover_body(self):
90
+ route = respx.post(f"{TEST_URL}/api/v1/discover").mock(
91
+ return_value=httpx.Response(
92
+ 200, json={"tools": [RANKED_TOOL.model_dump()]}
93
+ )
94
+ )
95
+
96
+ async with Agentified(
97
+ AgentifiedConfig(server_url=TEST_URL, tools=[TEST_TOOL])
98
+ ) as agent:
99
+ await agent.prefetch(
100
+ messages=[{"role": "user", "content": "test"}],
101
+ exclude=["frontendTool"],
102
+ )
103
+
104
+ import json
105
+
106
+ body = json.loads(route.calls[0].request.content)
107
+ assert body["exclude"] == ["frontendTool"]
108
+
109
+ @respx.mock
110
+ async def test_omits_exclude_when_not_provided(self):
111
+ route = respx.post(f"{TEST_URL}/api/v1/discover").mock(
112
+ return_value=httpx.Response(200, json={"tools": []})
113
+ )
114
+
115
+ async with Agentified(
116
+ AgentifiedConfig(server_url=TEST_URL, tools=[TEST_TOOL])
117
+ ) as agent:
118
+ await agent.prefetch(messages=[{"role": "user", "content": "test"}])
119
+
120
+ import json
121
+
122
+ body = json.loads(route.calls[0].request.content)
123
+ assert "exclude" not in body
124
+
125
+ @respx.mock
126
+ async def test_passes_turn_id_to_discover_body(self):
127
+ route = respx.post(f"{TEST_URL}/api/v1/discover").mock(
128
+ return_value=httpx.Response(
129
+ 200, json={"tools": [RANKED_TOOL.model_dump()]}
130
+ )
131
+ )
132
+
133
+ async with Agentified(
134
+ AgentifiedConfig(server_url=TEST_URL, tools=[TEST_TOOL])
135
+ ) as agent:
136
+ await agent.prefetch(
137
+ messages=[{"role": "user", "content": "test"}],
138
+ limit=5,
139
+ turn_id="turn-xyz",
140
+ )
141
+
142
+ import json
143
+
144
+ body = json.loads(route.calls[0].request.content)
145
+ assert body["turn_id"] == "turn-xyz"
146
+
147
+ @respx.mock
148
+ async def test_omits_turn_id_when_not_provided(self):
149
+ route = respx.post(f"{TEST_URL}/api/v1/discover").mock(
150
+ return_value=httpx.Response(200, json={"tools": []})
151
+ )
152
+
153
+ async with Agentified(
154
+ AgentifiedConfig(server_url=TEST_URL, tools=[TEST_TOOL])
155
+ ) as agent:
156
+ await agent.prefetch(messages=[{"role": "user", "content": "test"}])
157
+
158
+ import json
159
+
160
+ body = json.loads(route.calls[0].request.content)
161
+ assert "turn_id" not in body
162
+
163
+
164
+ class TestAsDiscoverTool:
165
+ @respx.mock
166
+ async def test_returns_definition_and_calls_discover(self):
167
+ respx.post(f"{TEST_URL}/api/v1/discover").mock(
168
+ return_value=httpx.Response(
169
+ 200, json={"tools": [RANKED_TOOL.model_dump()]}
170
+ )
171
+ )
172
+
173
+ async with Agentified(
174
+ AgentifiedConfig(server_url=TEST_URL, tools=[TEST_TOOL])
175
+ ) as agent:
176
+ discover_tool = agent.as_discover_tool()
177
+
178
+ assert discover_tool.definition.name == "agentified_discover"
179
+ assert "query" in discover_tool.definition.parameters["properties"]
180
+
181
+ result = await discover_tool.execute({"query": "weather tools", "limit": 3})
182
+ assert len(result) == 1
183
+ assert result[0].name == "get_weather"
184
+
185
+ @respx.mock
186
+ async def test_emits_start_and_complete_events(self):
187
+ respx.post(f"{TEST_URL}/api/v1/discover").mock(
188
+ return_value=httpx.Response(
189
+ 200, json={"tools": [RANKED_TOOL.model_dump()]}
190
+ )
191
+ )
192
+
193
+ events: list[AgentifiedEvent] = []
194
+
195
+ async with Agentified(
196
+ AgentifiedConfig(
197
+ server_url=TEST_URL,
198
+ tools=[TEST_TOOL],
199
+ on_event=lambda e: events.append(e),
200
+ )
201
+ ) as agent:
202
+ await agent.as_discover_tool().execute({"query": "weather"})
203
+
204
+ assert len(events) == 2
205
+ assert events[0].type == "agentified:discover:start"
206
+ assert events[0].query == "weather"
207
+ assert events[1].type == "agentified:discover:complete"
208
+ assert events[1].duration_ms >= 0
209
+
210
+
211
+ class TestGetFrontendTools:
212
+ def test_returns_tools_with_frontend_location(self):
213
+ frontend_tool = ServerTool(
214
+ name="confirm_action",
215
+ description="Confirm an action",
216
+ parameters={},
217
+ metadata={"location": "frontend"},
218
+ )
219
+ server_tool = ServerTool(
220
+ name="get_data",
221
+ description="Get data",
222
+ parameters={},
223
+ )
224
+
225
+ agent = Agentified(
226
+ AgentifiedConfig(
227
+ server_url=TEST_URL, tools=[frontend_tool, server_tool]
228
+ )
229
+ )
230
+ assert agent.get_frontend_tools() == [frontend_tool]
231
+
232
+ def test_returns_empty_when_no_frontend_tools(self):
233
+ agent = Agentified(
234
+ AgentifiedConfig(server_url=TEST_URL, tools=[TEST_TOOL])
235
+ )
236
+ assert agent.get_frontend_tools() == []
237
+
238
+
239
+ class TestGetFrontendToolNames:
240
+ def test_returns_names_of_frontend_tools(self):
241
+ frontend_tool = ServerTool(
242
+ name="confirm_action",
243
+ description="Confirm",
244
+ parameters={},
245
+ metadata={"location": "frontend"},
246
+ )
247
+
248
+ agent = Agentified(
249
+ AgentifiedConfig(
250
+ server_url=TEST_URL, tools=[frontend_tool, TEST_TOOL]
251
+ )
252
+ )
253
+ assert agent.get_frontend_tool_names() == ["confirm_action"]
254
+
255
+
256
+ class TestCaptureTurn:
257
+ @respx.mock
258
+ async def test_posts_turn_data_and_returns_turn_id(self):
259
+ route = respx.post(f"{TEST_URL}/api/v1/turns").mock(
260
+ return_value=httpx.Response(201, json={"turn_id": "abc-123"})
261
+ )
262
+
263
+ async with Agentified(
264
+ AgentifiedConfig(server_url=TEST_URL, tools=[TEST_TOOL])
265
+ ) as agent:
266
+ result = await agent.capture_turn(
267
+ tools_loaded=["get_weather"],
268
+ message="What is the weather?",
269
+ )
270
+
271
+ assert result.turn_id == "abc-123"
272
+ assert route.called
273
+
274
+
275
+ class TestOnEventOptional:
276
+ @respx.mock
277
+ async def test_does_not_crash_when_on_event_not_provided(self):
278
+ respx.post(f"{TEST_URL}/api/v1/discover").mock(
279
+ return_value=httpx.Response(
280
+ 200, json={"tools": [RANKED_TOOL.model_dump()]}
281
+ )
282
+ )
283
+
284
+ async with Agentified(
285
+ AgentifiedConfig(server_url=TEST_URL, tools=[TEST_TOOL])
286
+ ) as agent:
287
+ result = await agent.prefetch(
288
+ messages=[{"role": "user", "content": "test"}]
289
+ )
290
+ assert len(result) == 1
291
+
292
+ result2 = await agent.as_discover_tool().execute({"query": "test"})
293
+ assert len(result2) == 1
@@ -0,0 +1,72 @@
1
+ import httpx
2
+ import respx
3
+
4
+ from agentified._sync import SyncAgentified
5
+ from agentified.models import AgentifiedConfig, RankedTool, ServerTool
6
+
7
+ TEST_URL = "http://localhost:9119"
8
+
9
+ TEST_TOOL = ServerTool(
10
+ name="get_weather",
11
+ description="Get weather for a city",
12
+ parameters={"type": "object", "properties": {"city": {"type": "string"}}},
13
+ )
14
+
15
+ RANKED_TOOL = RankedTool(**TEST_TOOL.model_dump(), score=0.95)
16
+
17
+
18
+ class TestSyncAgentified:
19
+ @respx.mock
20
+ def test_register_sync(self):
21
+ respx.post(f"{TEST_URL}/api/v1/tools").mock(
22
+ return_value=httpx.Response(200, json={"registered": 1})
23
+ )
24
+
25
+ client = SyncAgentified(
26
+ AgentifiedConfig(server_url=TEST_URL, tools=[TEST_TOOL])
27
+ )
28
+ result = client.register()
29
+ assert result.registered == 1
30
+
31
+ @respx.mock
32
+ def test_prefetch_sync(self):
33
+ respx.post(f"{TEST_URL}/api/v1/discover").mock(
34
+ return_value=httpx.Response(
35
+ 200, json={"tools": [RANKED_TOOL.model_dump()]}
36
+ )
37
+ )
38
+
39
+ client = SyncAgentified(
40
+ AgentifiedConfig(server_url=TEST_URL, tools=[TEST_TOOL])
41
+ )
42
+ result = client.prefetch(
43
+ messages=[{"role": "user", "content": "weather"}]
44
+ )
45
+ assert len(result) == 1
46
+ assert result[0].score == 0.95
47
+
48
+ @respx.mock
49
+ def test_capture_turn_sync(self):
50
+ respx.post(f"{TEST_URL}/api/v1/turns").mock(
51
+ return_value=httpx.Response(201, json={"turn_id": "abc-123"})
52
+ )
53
+
54
+ client = SyncAgentified(
55
+ AgentifiedConfig(server_url=TEST_URL, tools=[TEST_TOOL])
56
+ )
57
+ result = client.capture_turn(
58
+ tools_loaded=["get_weather"], message="What is the weather?"
59
+ )
60
+ assert result.turn_id == "abc-123"
61
+
62
+ def test_get_frontend_tools_sync(self):
63
+ frontend_tool = ServerTool(
64
+ name="confirm",
65
+ description="Confirm",
66
+ parameters={},
67
+ metadata={"location": "frontend"},
68
+ )
69
+ client = SyncAgentified(
70
+ AgentifiedConfig(server_url=TEST_URL, tools=[frontend_tool, TEST_TOOL])
71
+ )
72
+ assert client.get_frontend_tool_names() == ["confirm"]
@@ -0,0 +1,39 @@
1
+ import json
2
+
3
+ from agentified.tool import tool
4
+
5
+
6
+ def test_converts_definition_to_server_tool_with_fields():
7
+ result = tool(
8
+ name="get_weather",
9
+ description="Get weather for a city",
10
+ parameters={
11
+ "type": "object",
12
+ "properties": {"city": {"type": "string"}},
13
+ },
14
+ )
15
+
16
+ assert result.name == "get_weather"
17
+ assert result.description == "Get weather for a city"
18
+ assert result.parameters == {
19
+ "type": "object",
20
+ "properties": {"city": {"type": "string"}},
21
+ }
22
+ assert result.metadata is None
23
+ assert result.fields is not None
24
+ assert result.fields.name == "get_weather"
25
+ assert result.fields.description == "Get weather for a city"
26
+ assert result.fields.input_schema == json.dumps(
27
+ {"type": "object", "properties": {"city": {"type": "string"}}}
28
+ )
29
+
30
+
31
+ def test_preserves_metadata_when_provided():
32
+ result = tool(
33
+ name="confirm",
34
+ description="Confirm action",
35
+ parameters={},
36
+ metadata={"location": "frontend"},
37
+ )
38
+
39
+ assert result.metadata == {"location": "frontend"}