attp-client 0.0.8__py3-none-any.whl → 0.0.10__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.
attp_client/catalog.py CHANGED
@@ -28,6 +28,7 @@ class AttpCatalog:
28
28
  self.tool_manager = manager
29
29
  self.attached_tools = {}
30
30
  self.tool_name_to_id_symlink = {}
31
+ self.disposable = None
31
32
 
32
33
  self.responder = self.tool_manager.router.responder
33
34
 
@@ -59,14 +60,20 @@ class AttpCatalog:
59
60
  from reactivex import empty
60
61
  return empty()
61
62
 
62
- self.responder.pipe(
63
- ops.filter(lambda item: item.payload is not None and item.correlation_id == 1),
63
+ self.disposable = self.responder.pipe(
64
+ ops.filter(lambda item: item.payload is not None and item.route_id == 2),
64
65
  ops.map(lambda item: envelopize(item)),
65
66
  ops.catch(catch_handler),
66
67
  ops.filter(lambda item: item.catalog == self.catalog_name and item.tool_id in self.attached_tools),
67
68
  ops.observe_on(scheduler),
68
69
  ).subscribe(lambda item: handle_call(item))
69
70
 
71
+ async def handle_callback(self, envelope: IEnvelope) -> Any:
72
+ if envelope.tool_id not in self.attached_tools:
73
+ raise NotFoundError(f"Tool {envelope.tool_id} not marked as registered and wasn't found in the catalog {self.catalog_name}.")
74
+
75
+ return await self.handle_call(envelope)
76
+
70
77
  async def attach_tool(
71
78
  self,
72
79
  callback: Callable[[IEnvelope], Any],
attp_client/client.py CHANGED
@@ -1,19 +1,25 @@
1
1
  import asyncio
2
2
  from functools import cached_property
3
3
  from logging import Logger, getLogger
4
- from typing import Any, Callable, Literal
4
+ from typing import Any, Callable
5
5
  from attp_core.rs_api import AttpClientSession, Limits
6
- from reactivex import Subject
6
+ from reactivex import Subject, operators as ops
7
+ from reactivex.scheduler.eventloop import AsyncIOScheduler
7
8
  from attp_core.rs_api import PyAttpMessage
8
9
 
9
10
  from attp_client.catalog import AttpCatalog
11
+ from attp_client.errors.dead_session import DeadSessionError
10
12
  from attp_client.inference import AttpInferenceAPI
11
13
  from attp_client.interfaces.catalogs.catalog import ICatalogResponse
14
+ from attp_client.interfaces.error import IErr
12
15
  from attp_client.misc.serializable import Serializable
13
16
  from attp_client.router import AttpRouter
14
17
  from attp_client.session import SessionDriver
15
18
  from attp_client.tools import ToolsManager
16
19
  from attp_client.types.route_mapping import AttpRouteMapping, RouteType
20
+ from attp_client.utils import envelopizer
21
+
22
+ from attp_core.rs_api import AttpCommand
17
23
 
18
24
  class ATTPClient:
19
25
 
@@ -22,7 +28,8 @@ class ATTPClient:
22
28
  session: SessionDriver | None
23
29
  routes: list[AttpRouteMapping]
24
30
  inference: AttpInferenceAPI
25
-
31
+ catalogs: list[AttpCatalog]
32
+
26
33
  def __init__(
27
34
  self,
28
35
  agt_token: str,
@@ -37,20 +44,22 @@ class ATTPClient:
37
44
  self.organization_id = organization_id
38
45
  self.connection_url = connection_url or "attp://localhost:6563"
39
46
 
40
- self.client = AttpClientSession(self.connection_url)
41
47
  self.session = None
42
48
  self.max_retries = max_retries
43
49
  self.limits = limits or Limits(max_payload_size=50000)
50
+ self.client = AttpClientSession(self.connection_url, limits=self.limits)
44
51
  self.logger = logger
45
52
 
46
53
  self.route_increment_index = 2
47
54
 
48
55
  self.responder = Subject[PyAttpMessage]()
49
56
  self.routes = []
57
+ self.catalogs = []
58
+ self.disposable = None
50
59
 
51
60
  async def connect(self):
52
61
  # Open the connection
53
- client = await self.client.connect(self.max_retries, self.limits)
62
+ client = await self.client.connect(self.max_retries)
54
63
 
55
64
  if not client.session:
56
65
  raise ConnectionError("Failed to connect to ATTP server after 10 attempts!")
@@ -69,6 +78,17 @@ class ATTPClient:
69
78
 
70
79
  self.router = AttpRouter(self.responder, self.session)
71
80
  self.inference = AttpInferenceAPI(self.router)
81
+
82
+ self.add_event_handler("tools:call", "message", self._tool_callback)
83
+
84
+
85
+
86
+ self.disposable = self.responder.pipe(
87
+ ops.subscribe_on(AsyncIOScheduler(asyncio.get_event_loop())),
88
+ ).subscribe(
89
+ on_next=lambda item: self.logger.debug(f"Received message on route {item.route_id} with correlation ID {item.correlation_id}"),
90
+ on_error=lambda e: self.logger.error(f"Error in responder stream: {e}"),
91
+ )
72
92
 
73
93
  async def close(self):
74
94
  if self.session:
@@ -81,14 +101,82 @@ class ATTPClient:
81
101
  return ToolsManager(self.router)
82
102
 
83
103
  async def catalog(self, catalog_name: str):
104
+ if any(c.catalog_name == catalog_name for c in self.catalogs):
105
+ return next(c for c in self.catalogs if c.catalog_name == catalog_name)
106
+
84
107
  catalog = await self.router.send(
85
108
  "tools:catalogs:specific",
86
109
  Serializable[dict[str, str]]({"catalog_name": catalog_name}),
87
110
  timeout=10,
88
111
  expected_response=ICatalogResponse
89
112
  )
113
+ self.catalogs.append(
114
+ AttpCatalog(id=catalog.catalog_id, catalog_name=catalog_name, manager=self.tools)
115
+ )
116
+
117
+ await self.catalogs[-1].start_tool_listener()
118
+
119
+ return self.catalogs[-1] # Return the newly added catalog
120
+
121
+ async def close_catalog(self, catalog: AttpCatalog):
122
+ await catalog.detach_all_tools()
123
+ self.catalogs.remove(catalog)
124
+
125
+ async def _tool_callback(self, message: PyAttpMessage):
126
+ if not self.session:
127
+ raise DeadSessionError(self.organization_id)
90
128
 
91
- return AttpCatalog(id=catalog.catalog_id, catalog_name=catalog_name, manager=self.tools)
129
+ if not message.correlation_id:
130
+ await self.session.send_error(IErr(
131
+ detail={"message": "Correlation ID was missing in the message.", "code": "MissingCorrelationId"},
132
+ ))
133
+ return
134
+
135
+ print("TOOL CALLBACK MESSAGE:", message.payload)
136
+ try:
137
+ envelope = envelopizer.envelopize(message)
138
+ except ValueError as e:
139
+ await self.session.send_error(IErr(
140
+ detail={"message": str(e), "code": "InvalidPayload"},
141
+ ))
142
+ return
143
+
144
+ catalog = next((c for c in self.catalogs if c.id == envelope.tool_id), None)
145
+ if not catalog:
146
+ await self.session.send_error(IErr(
147
+ detail={"message": f"Catalog with id {envelope.tool_id} not found.", "code": "NotFoundError"},
148
+ ))
149
+ return
150
+
151
+ response = await catalog.handle_callback(envelope)
152
+
153
+ await self.session.respond(route=message.route_id, correlation_id=message.correlation_id, payload=response)
154
+
155
+ async def _handle_incoming(self, message: PyAttpMessage):
156
+ relevant_route = next((route for route in self.routes if route.route_id == message.route_id), None)
157
+
158
+ if not relevant_route:
159
+ if self.logger:
160
+ self.logger.warning(f"Received message for unknown route ID {message.route_id}. Ignoring.")
161
+ return
162
+
163
+ response = await relevant_route.callback(message)
164
+
165
+ if message.command_type == AttpCommand.CALL:
166
+ if not self.session:
167
+ raise DeadSessionError(self.organization_id)
168
+
169
+ if not message.correlation_id:
170
+ await self.session.send_error(IErr(
171
+ detail={"message": "Correlation ID was missing in the message.", "code": "MissingCorrelationId"},
172
+ ))
173
+ return
174
+
175
+ await self.session.respond(
176
+ route=message.route_id,
177
+ correlation_id=message.correlation_id,
178
+ payload=response
179
+ )
92
180
 
93
181
  def add_event_handler(
94
182
  self,
attp_client/inference.py CHANGED
@@ -18,6 +18,79 @@ class AttpInferenceAPI:
18
18
  self.router = router
19
19
  self.logger = logger
20
20
 
21
+ async def create_chat(
22
+ self,
23
+ name: str,
24
+ agent_id: int | None = None,
25
+ agent_name: str | None = None,
26
+ mode: str = "agent_autopilot",
27
+ platform: str = "unknown",
28
+ responsible: int | None = None,
29
+ client_id: str | None = None,
30
+ created_by_id: int | None = None,
31
+ timeout: float = 30
32
+ ):
33
+ """
34
+ Create chat for inference.
35
+ TODO: Implement own chat manager for chats, just like I did with catalogs.
36
+ """
37
+ response = await self.router.send(
38
+ "messages:chat:create",
39
+ Serializable[dict[str, Any]]({
40
+ "name": name,
41
+ "agent_id": agent_id,
42
+ "agent_name": agent_name,
43
+ "mode": mode,
44
+ "platform": platform,
45
+ "responsible": responsible,
46
+ "client_id": client_id,
47
+ "created_by_id": created_by_id
48
+ }),
49
+ timeout=timeout,
50
+ expected_response=dict[str, Any]
51
+ )
52
+
53
+ return response
54
+
55
+ async def change_chat_agent(
56
+ self,
57
+ chat_id: UUID,
58
+ agent_id: int | None = None,
59
+ agent_name: str | None = None,
60
+ timeout: float = 30
61
+ ) -> dict[str, Any]:
62
+ """
63
+ Change the agent associated with a chat.
64
+
65
+ Parameters
66
+ ----------
67
+ chat_id : UUID
68
+ The ID of the chat to change the agent for.
69
+ agent_id : int | None, optional
70
+ The ID of the new agent to associate with the chat, by default None.
71
+ agent_name : str | None, optional
72
+ The name of the new agent to associate with the chat, by default None.
73
+ timeout : float, optional
74
+ The timeout for the request, by default 30.
75
+
76
+ Returns
77
+ -------
78
+ dict[str, Any]
79
+ The response from the change agent request.
80
+ """
81
+ response = await self.router.send(
82
+ "messages:chat:change_agent",
83
+ Serializable[dict[str, Any]]({
84
+ "chat_id": str(chat_id),
85
+ "agent_id": agent_id,
86
+ "agent_name": agent_name
87
+ }),
88
+ timeout=timeout,
89
+ expected_response=dict[str, Any]
90
+ )
91
+
92
+ return response
93
+
21
94
  async def invoke_inference(
22
95
  self,
23
96
  agent_id: int | None = None,
attp_client/session.py CHANGED
@@ -215,7 +215,7 @@ class SessionDriver:
215
215
  )
216
216
  )
217
217
 
218
- async def respond(self, correlation_id: bytes, payload: FixedBaseModel | Any | None = None):
218
+ async def respond(self, route: int | str, correlation_id: bytes, payload: FixedBaseModel | Serializable | Any | None = None):
219
219
  """
220
220
  For responding to `AttpCommand.CALL`. Used only for correlated requests.
221
221
  It sends response (acknowledgement) message signed as `AttpCommand.ACK` to the request.
@@ -227,8 +227,16 @@ class SessionDriver:
227
227
  payload : FixedBaseModel | Serializable | None, optional
228
228
  Response payload, the data that will be sent, by default None
229
229
  """
230
+ relevant_route = route
231
+
232
+ if not self.server_routes:
233
+ raise UnauthenticatedError(f"Cannot send an ATTP message with acknowledgement to unauthenticated (route_mapping={route})")
234
+
235
+ if isinstance(route, str):
236
+ relevant_route = resolve_route_by_id("message", route, self.server_routes).route_id
237
+
230
238
  frame = PyAttpMessage(
231
- route_id=0,
239
+ route_id=int(relevant_route),
232
240
  command_type=AttpCommand.ACK,
233
241
  correlation_id=correlation_id,
234
242
  payload=serializer.deserialize(payload),
@@ -310,6 +318,7 @@ class SessionDriver:
310
318
  if not self.session:
311
319
  raise DeadSessionError(self.organization_id)
312
320
  self.session.add_event_handler(self._on_event)
321
+
313
322
  await asyncio.gather(
314
323
  self.session.start_handler(),
315
324
  self.session.start_listener()
attp_client/tools.py CHANGED
@@ -51,7 +51,7 @@ class ToolsManager:
51
51
  tool_id: str | Sequence[str]
52
52
  ) -> str | list[str]:
53
53
  response = await self.router.send(
54
- "tool:unregister",
54
+ "tools:unregister",
55
55
  Serializable[dict[str, Any]]({
56
56
  "catalog": catalog_name,
57
57
  "tool_id": tool_id
@@ -0,0 +1,9 @@
1
+ from attp_client.interfaces.catalogs.tools.envelope import IEnvelope
2
+ from attp_core.rs_api import PyAttpMessage
3
+
4
+
5
+ def envelopize(message: PyAttpMessage) -> IEnvelope:
6
+ if not message.payload:
7
+ raise ValueError("Message payload is empty, cannot envelopize.")
8
+
9
+ return IEnvelope.mps(message.payload)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: attp-client
3
- Version: 0.0.8
3
+ Version: 0.0.10
4
4
  Summary: A python-sdk client for interacting with AgentHub's ATTP protocol (Agent Tool Transport Protocol)
5
5
  License: MIT
6
6
  Author: Ascender Team
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
13
  Requires-Dist: ascender-framework (>=2.0rc7,<3.0)
14
- Requires-Dist: attp-core (==0.1.10)
14
+ Requires-Dist: attp-core (==0.1.13)
15
15
  Requires-Dist: msgpack (>=1.1.1,<2.0.0)
16
16
  Requires-Dist: pydantic (>=2.11.7,<3.0.0)
17
17
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  attp_client/__init__.py,sha256=25qGMUU_W8juBuW9N6318vWoqALrPQSp3ECHHkNzUcg,2749
2
- attp_client/catalog.py,sha256=fDvFC-aherMGsFDRmUgYqlp77II78SXJlal_B4_Vkyg,4569
3
- attp_client/client.py,sha256=aSjDaH1pHkmPsVZBwIrSIMX8QXDtbDfGpVBXR5KgnF4,3989
2
+ attp_client/catalog.py,sha256=TLQvNKx3GtEYcyqWCJuvF6soZXeebKn6wfQNcvV4cX0,4921
3
+ attp_client/client.py,sha256=CMDj0NMBGDtGGQqz340L2-xafZfpNIxA2zPYdPbkDps,7531
4
4
  attp_client/consts.py,sha256=6UZyqWddycOp-TKW5yvb0JW2fdfcS-J2xFVVNfuJQbU,18
5
5
  attp_client/errors/attp_exception.py,sha256=HePYyYMknS4t6tMrwU-p9L_LPdj1i7chntrlM53ueK4,347
6
6
  attp_client/errors/correlated_rpc_exception.py,sha256=GivYlF0rC4zxbEt5sMxc1cO-iGxIuiEjTy7knN_iur4,591
@@ -8,7 +8,7 @@ attp_client/errors/dead_session.py,sha256=BI-EuTqxVP7j23wcD0GfhteyPsKR2xqUsMNm9S
8
8
  attp_client/errors/not_found.py,sha256=Y-Y_Mki1hQYihJttvO0ugHFu9-73--1wqwdOomp2IEM,39
9
9
  attp_client/errors/serialization_error.py,sha256=Pa8PRzFJrrikA1Ikj0q-0euvXVUMb_qj-NRIp55SfOk,198
10
10
  attp_client/errors/unauthenticated_error.py,sha256=F0V1FjO0qVLMl6Y120y3AXKZnwb5iDD17c4GEMbL5aI,46
11
- attp_client/inference.py,sha256=S_iAvUwROUIE4IJGC8-ZmVXJc1CP1nbbBHmR0U1k4ZQ,4655
11
+ attp_client/inference.py,sha256=P5fxaH8qlIaCVXpag5gZ_XblaVrpyTu5TRA8E6tE9VU,6955
12
12
  attp_client/interfaces/catalogs/catalog.py,sha256=3PxlRwR3y2tbQVfXAkhDIv07AJPraMfH0c_pyi7Y6z8,146
13
13
  attp_client/interfaces/catalogs/tools/envelope.py,sha256=6aUx06ou9If9OYv4BODKiBybrgBL2YWfSHZ6ukIR1K0,693
14
14
  attp_client/interfaces/error.py,sha256=fIrk5XlAhMs6mbYQ5PzgwS0v-LIbtne3OlQue4CjWXs,139
@@ -24,12 +24,13 @@ attp_client/interfaces/route_mappings.py,sha256=j6hEdkCP5xPpHS16EWmlkdTlnHa7z6e8
24
24
  attp_client/misc/fixed_basemodel.py,sha256=0MTVmlTrA75Oxv0pVfLdXFTSp5AmBzgiNwvDiLFGF_w,1853
25
25
  attp_client/misc/serializable.py,sha256=tU08TsjiLiafAhU1jKd5BxajlHdEDcdKeEiKPqhMSTI,2102
26
26
  attp_client/router.py,sha256=UDHU2xsvTjgSIzMtV0jkPvOYK9R7GFjKfIrjjBHws-Q,4575
27
- attp_client/session.py,sha256=oZx3Lgsr-y4onaL4yrM3Sj2caQck9wc14QcakhS42Js,11410
28
- attp_client/tools.py,sha256=zyMCDbph77Ojm74bEQhGm3gzam8lolVpLobcOc89dTM,1920
27
+ attp_client/session.py,sha256=80S1KAhiqiw-sAdT18yB1Wx7XRjol7drMAnYts2FYVs,11813
28
+ attp_client/tools.py,sha256=ThrqM3cL6_XaJdEn3f41P5Y6afct7_GaJ1-Va_ftdHU,1921
29
29
  attp_client/types/route_mapping.py,sha256=Kb9ZX88lqihRZr8IryfH1Vg_YAobW699Yjl6Raz1rdg,375
30
30
  attp_client/utils/context_awaiter.py,sha256=oCptu5g8mY43j5cr-W4fOe85OCCaqQI9r_Pn92NgZSY,1035
31
+ attp_client/utils/envelopizer.py,sha256=wnFEnEEDGx8eW-UYlhpTeOTyLLXnjBYw3-BOHWG-Hhk,310
31
32
  attp_client/utils/route_mapper.py,sha256=uJNhKp6ipCSUxoiZS0Liix2lHOgUAnJM0kfgXWAh9RQ,554
32
33
  attp_client/utils/serializer.py,sha256=O1tWYbQS9jC9aus-ISKtliKCgGmOEIb9hxykVrmMKGY,636
33
- attp_client-0.0.8.dist-info/METADATA,sha256=4muzno9jFdb6WrogPMa4b0HFIvD1LbNeWB5OCUUkVP8,7137
34
- attp_client-0.0.8.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
35
- attp_client-0.0.8.dist-info/RECORD,,
34
+ attp_client-0.0.10.dist-info/METADATA,sha256=pTVMigVQ-9bYTHxFnu1Rw0RJ7khCCX4SYkEIvlPeOn8,7138
35
+ attp_client-0.0.10.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
36
+ attp_client-0.0.10.dist-info/RECORD,,