vital-agent-container-sdk 0.1.0__py3-none-any.whl → 0.1.2__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.
@@ -0,0 +1,133 @@
1
+ import asyncio
2
+ import gc
3
+ import json
4
+ import sys
5
+ import tracemalloc
6
+ import uvicorn
7
+ import logging
8
+ from logging.handlers import RotatingFileHandler
9
+ from pythonjsonlogger import jsonlogger
10
+ import httpx
11
+ from dotenv import load_dotenv
12
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
13
+ from vital_agent_container.tasks.task_manager_async_client import TaskManagerAsyncClient
14
+ from vital_agent_container.utils.aws_utils import AWSUtils
15
+ from vital_agent_container.utils.config_utils import ConfigUtils
16
+ from vital_agent_container.processor.aimp_message_processor import AIMPMessageProcessor
17
+
18
+
19
+ logger = logging.getLogger("VitalAgentContainerLogger")
20
+ logger.setLevel(logging.INFO)
21
+
22
+ formatter = jsonlogger.JsonFormatter('%(asctime)s %(name)s %(levelname)s %(message)s')
23
+
24
+ stream_handler = logging.StreamHandler(sys.stdout)
25
+ stream_handler.setFormatter(formatter)
26
+
27
+ log_file = "/var/log/agentcontainer/app.log"
28
+
29
+ file_handler = RotatingFileHandler(log_file, maxBytes=5 * 1024 * 1024, backupCount=5)
30
+ file_handler.setFormatter(formatter)
31
+
32
+ logger.addHandler(stream_handler)
33
+ logger.addHandler(file_handler)
34
+
35
+ service_identifier = AWSUtils.get_task_arn()
36
+
37
+
38
+ class AgentContainerApp(FastAPI):
39
+ def __init__(self, handler, app_home, **kwargs):
40
+ super().__init__(**kwargs)
41
+ self.handler = handler
42
+ self.app_home = app_home
43
+ load_dotenv()
44
+ self.config = ConfigUtils.load_config(app_home)
45
+ self.message_processor = AIMPMessageProcessor()
46
+ self.add_routes()
47
+
48
+ async def process_ws_message(self, client: httpx.AsyncClient, websocket: WebSocket, data: str,
49
+ started_event: asyncio.Event):
50
+ print(f"process_ws_message: Processing: {data}")
51
+ await self.message_processor.process_message(self.handler, self.config, client, websocket, data, started_event)
52
+
53
+ def add_routes(self):
54
+ @self.get("/health")
55
+ async def health_check():
56
+ logger.info("health check")
57
+ return {"status": "ok"}
58
+
59
+ @self.websocket("/ws")
60
+ async def websocket_endpoint(websocket: WebSocket):
61
+ logger.info(f"WebSocket connection from {websocket.client.host}:{websocket.client.port} accepted.")
62
+ await websocket.accept()
63
+ client = TaskManagerAsyncClient()
64
+ client._ws_active = True
65
+ background_tasks = []
66
+ try:
67
+ while True:
68
+ data = await websocket.receive_text()
69
+
70
+ logger.info(f"Received message from {websocket.client.host}:{websocket.client.port}: {data}")
71
+ message_obj = json.loads(data)
72
+ logger.info(f"Received message: {message_obj}")
73
+ message_type = message_obj[0].get("type", None)
74
+ message_intent = message_obj[0].get("http://vital.ai/ontology/vital-aimp#hasIntent", None)
75
+ logger.info(f"message_type: {message_type}")
76
+ logger.info(f"message_intent: {message_intent}")
77
+
78
+ if message_intent == "interrupt":
79
+ logger.info("Processing interrupted by client.")
80
+ client.log_current_tasks()
81
+ await client.cancel_current_tasks()
82
+ for task in background_tasks:
83
+ task.cancel()
84
+ try:
85
+ await asyncio.gather(*background_tasks, return_exceptions=True)
86
+ except Exception as e:
87
+ logger.error(f"An error occurred in gather: {e}")
88
+ try:
89
+ if client.ws_active:
90
+ await websocket.close()
91
+ client._ws_active = False
92
+ except Exception as e:
93
+ logger.error(f"An error occurred in interrupt websocket close: {e}")
94
+ break
95
+
96
+ if len(background_tasks) > 0:
97
+ logger.info("currently processing task, ignoring new request.")
98
+ await websocket.send_text("processing task. ignoring message.")
99
+ else:
100
+ logger.info(f"Processing message: {data}")
101
+ started_event = asyncio.Event()
102
+ task = asyncio.create_task(self.process_ws_message(client, websocket, data, started_event))
103
+ background_tasks.append(task)
104
+ await started_event.wait()
105
+ logger.info(f"Completed Processing message: {data}")
106
+ # break out of infinite loop
107
+ break
108
+ except WebSocketDisconnect:
109
+ logger.info("WebSocket connection closed by the client.")
110
+ # still try to close it in finally?
111
+ # client._ws_active = False
112
+ except Exception as e:
113
+ logger.error(f"An error occurred in ws main loop: {e}")
114
+ try:
115
+ await websocket.close()
116
+ client._ws_active = False
117
+ except Exception as e2:
118
+ logger.error(f"An error occurred in websocket close: {e2}")
119
+ finally:
120
+ try:
121
+ await asyncio.gather(*background_tasks)
122
+ except Exception as e:
123
+ logger.error(f"An error occurred in final gather: {e}")
124
+ try:
125
+ if client.ws_active:
126
+ await websocket.close()
127
+ except Exception as e3:
128
+ logger.error(f"An error occurred in finally websocket close: {e3}")
129
+ await client.cancel_current_tasks()
130
+
131
+ @self.on_event("shutdown")
132
+ async def shutdown_event():
133
+ logger.info("Shutting down...")
File without changes
File without changes
@@ -0,0 +1,14 @@
1
+ import asyncio
2
+ import httpx
3
+ from starlette.websockets import WebSocket
4
+ from abc import ABC, abstractmethod
5
+
6
+
7
+ class AIMPMessageHandlerInf(ABC):
8
+
9
+ @abstractmethod
10
+ async def process_message(self, config, client: httpx.AsyncClient, websocket: WebSocket, data: str, started_event: asyncio.Event):
11
+ pass
12
+
13
+
14
+
File without changes
@@ -0,0 +1,23 @@
1
+ import asyncio
2
+ import httpx
3
+ from starlette.websockets import WebSocket
4
+ from vital_agent_container.handler.aimp_message_handler_inf import AIMPMessageHandlerInf
5
+
6
+
7
+ class AIMPEchoMessageHandler(AIMPMessageHandlerInf):
8
+ async def process_message(self, config, client: httpx.AsyncClient, websocket: WebSocket, data: str, started_event: asyncio.Event):
9
+ try:
10
+ print(f"Received Message: {data}")
11
+ await websocket.send_text(data)
12
+ print(f"Sent Message: {data}")
13
+ # await websocket.close(1000, "Processing Complete")
14
+ # print(f"Websocket closed.")
15
+ started_event.set()
16
+ print(f"Completed Event.")
17
+ except asyncio.CancelledError:
18
+ # log canceling
19
+ raise
20
+
21
+
22
+
23
+
File without changes
@@ -0,0 +1,18 @@
1
+ import asyncio
2
+
3
+ from vital_agent_container.handler.aimp_message_handler_inf import AIMPMessageHandlerInf
4
+
5
+
6
+ class AIMPMessageProcessor:
7
+
8
+ def __init__(self):
9
+ pass
10
+
11
+ async def process_message(self, handler: AIMPMessageHandlerInf, app_config, client, websocket, data, started_event):
12
+ print(f"Processing: {data}")
13
+ try:
14
+ return await handler.process_message(app_config, client, websocket, data, started_event)
15
+ except asyncio.CancelledError:
16
+ # log canceling
17
+ raise
18
+
File without changes
@@ -0,0 +1,38 @@
1
+ import asyncio
2
+ import logging
3
+ import httpx
4
+
5
+
6
+ class CustomStreamingResponse:
7
+ def __init__(self, response: httpx.Response, end_event: asyncio.Event):
8
+ self._response = response
9
+ self._end_event = end_event
10
+
11
+ async def aiter_lines(self):
12
+ try:
13
+ async for line in self._response.aiter_lines():
14
+ yield line
15
+ finally:
16
+ self.handle_end_of_stream()
17
+
18
+ async def aiter_bytes(self):
19
+ try:
20
+ async for chunk in self._response.aiter_bytes():
21
+ yield chunk
22
+ finally:
23
+ self.handle_end_of_stream()
24
+
25
+ async def aiter_raw(self):
26
+ try:
27
+ async for chunk in self._response.aiter_raw():
28
+ yield chunk
29
+ finally:
30
+ self.handle_end_of_stream()
31
+
32
+ def handle_end_of_stream(self):
33
+ self._end_event.set()
34
+ logger = logging.getLogger("VitalAgentContainerLogger")
35
+ logger.info("Reached the end of the stream.")
36
+
37
+ def __getattr__(self, name):
38
+ return getattr(self._response, name)
File without changes
@@ -0,0 +1,21 @@
1
+ import asyncio
2
+ import httpx
3
+
4
+
5
+ class RequestTask:
6
+ def __init__(self):
7
+ self._request: httpx.Response = None
8
+ self._event: asyncio.Event = None
9
+ self._task: asyncio.Task = None
10
+
11
+ @property
12
+ def request(self):
13
+ return self._request
14
+
15
+ @property
16
+ def task(self):
17
+ return self._task
18
+
19
+ @property
20
+ def event(self):
21
+ return self._event
@@ -0,0 +1,64 @@
1
+ import asyncio
2
+ import logging
3
+ from typing import Dict
4
+ import httpx
5
+ from vital_agent_container.streaming.custom_streaming_response import CustomStreamingResponse
6
+ from vital_agent_container.tasks.request_task import RequestTask
7
+
8
+
9
+ class TaskManagerAsyncClient(httpx.AsyncClient):
10
+ def __init__(self, *args, **kwargs):
11
+ super().__init__(*args, **kwargs)
12
+ self._tasks: Dict[RequestTask, str] = {}
13
+ self._ws_active = None
14
+
15
+ @property
16
+ def ws_active(self):
17
+ return self._ws_active
18
+
19
+ async def send(self, request, *args, **kwargs):
20
+ is_streaming = kwargs.get("stream", False)
21
+
22
+ async def task_coro(rt: RequestTask):
23
+ response = await super(TaskManagerAsyncClient, self).send(request, *args, **kwargs)
24
+ if is_streaming:
25
+ end_event = asyncio.Event()
26
+ custom_response = CustomStreamingResponse(response, end_event)
27
+ rt._event = end_event
28
+ rt._request = custom_response
29
+ return custom_response
30
+ else:
31
+ return response
32
+
33
+ request_task = RequestTask()
34
+ task = asyncio.create_task(task_coro(request_task))
35
+ request_task._task = task
36
+ self._tasks[request_task] = request.url.path
37
+
38
+ try:
39
+ return await task
40
+ finally:
41
+ # self._tasks.pop(task, None)
42
+ pass
43
+
44
+ def log_current_tasks(self):
45
+ logger = logging.getLogger("VitalAgentContainerLogger")
46
+ if self._tasks:
47
+ logger.info(f"Current tasks: {', '.join(self._tasks.values())}")
48
+ else:
49
+ logger.info("No current tasks.")
50
+
51
+ async def cancel_current_tasks(self):
52
+ if self._tasks:
53
+ for rt in self._tasks.keys():
54
+ # res = rt.request
55
+ t = rt.task
56
+ status = t.cancel()
57
+ if status:
58
+ try:
59
+ await t
60
+ except asyncio.CancelledError:
61
+ pass
62
+ self._tasks.clear()
63
+ else:
64
+ pass
File without changes
@@ -0,0 +1,21 @@
1
+ import os
2
+ import requests
3
+
4
+
5
+ class AWSUtils:
6
+
7
+ @staticmethod
8
+ def get_task_arn():
9
+ metadata_uri = os.environ.get('ECS_CONTAINER_METADATA_URI_V4')
10
+ if not metadata_uri:
11
+ return "local-instance"
12
+ task_metadata_url = f"{metadata_uri}/task"
13
+
14
+ try:
15
+ response = requests.get(task_metadata_url)
16
+ response.raise_for_status()
17
+ metadata = response.json()
18
+ task_arn = metadata.get('TaskARN')
19
+ return task_arn
20
+ except requests.RequestException as e:
21
+ return "local-instance"
@@ -0,0 +1,17 @@
1
+ import logging
2
+ import os
3
+
4
+ import yaml
5
+
6
+
7
+ class ConfigUtils:
8
+
9
+ @staticmethod
10
+ def load_config(app_home):
11
+
12
+ with open(f"{app_home}/agent_config.yaml", "r") as config_stream:
13
+ try:
14
+ return yaml.safe_load(config_stream)
15
+ except yaml.YAMLError as exc:
16
+ logger = logging.getLogger("VitalAgentContainerLogger")
17
+ logger.info("failed to load config file")
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.1
2
+ Name: vital-agent-container-sdk
3
+ Version: 0.1.2
4
+ Summary: Vital Agent Container SDK
5
+ Home-page: https://github.com/vital-ai/vital-agent-container-python
6
+ Author: Marc Hadfield
7
+ Author-email: marc@vital.ai
8
+ License: Apache License 2.0
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: vital-ai-vitalsigns ==0.1.20
16
+ Requires-Dist: vital-ai-aimp ==0.1.7
17
+ Requires-Dist: httpx ==0.26.0
18
+ Requires-Dist: python-json-logger ==2.0.7
19
+ Requires-Dist: python-dotenv ==1.0.1
20
+ Requires-Dist: uvicorn[standard] ==0.27.0.post1
21
+ Requires-Dist: fastapi ==0.109.2
22
+ Requires-Dist: dataclasses-json ==0.5.7
23
+ Requires-Dist: aiohttp ==3.9.0
24
+ Requires-Dist: aiosignal ==1.2.0
25
+ Requires-Dist: anyio ==4.2.0
26
+ Requires-Dist: async-timeout ==4.0.3
27
+ Requires-Dist: starlette ==0.36.3
28
+ Requires-Dist: marshmallow ==3.19.0
29
+ Requires-Dist: pyyaml ==6.0.1
30
+ Requires-Dist: requests ==2.31.0
31
+ Requires-Dist: Pillow ==10.2.0
32
+ Requires-Dist: websockets ==12.0
33
+ Provides-Extra: dev
34
+
35
+ # vital-agent-container-python
@@ -0,0 +1,22 @@
1
+ vital_agent_container/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ vital_agent_container/agent_container_app.py,sha256=5lbge8OC8tEkjJ_TsZ9-zkRPPKA_Jr8gwwIFxmo7z6U,5879
3
+ vital_agent_container/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ vital_agent_container/handler/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ vital_agent_container/handler/aimp_message_handler_inf.py,sha256=H9Bey5fAAh13LscsmwlCkK6i-rMKe5iN_vwk279p57s,315
6
+ vital_agent_container/handler/impl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ vital_agent_container/handler/impl/aimp_echo_message_handler.py,sha256=QQl3MbAWONno408wXOXzlubhFRvp9Vjwk5iCiF12Sek,764
8
+ vital_agent_container/processor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ vital_agent_container/processor/aimp_message_processor.py,sha256=4aaMEKxpzpXuH-kjLyqGY8t5PkqFif7Dr5ZaFHKEr2o,532
10
+ vital_agent_container/streaming/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ vital_agent_container/streaming/custom_streaming_response.py,sha256=KDuhl4Z5opb0pxCymkjFvyBPKvhf64TZatLY-qK0l_w,1059
12
+ vital_agent_container/tasks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ vital_agent_container/tasks/request_task.py,sha256=l-cGxSuI0hKk_ZjI5nkMCWtwFYs4WDu2DXVtcWGIXSY,391
14
+ vital_agent_container/tasks/task_manager_async_client.py,sha256=hOkhGYgtdrpOkZygfYhjdENmfJViL7W8HTA2junHrkc,2081
15
+ vital_agent_container/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ vital_agent_container/utils/aws_utils.py,sha256=HuwdOs58VU9UDnytx2QlKjJNoPE9k2KIQbKJwgi-9zk,580
17
+ vital_agent_container/utils/config_utils.py,sha256=0iqBXct1O0nMTTBkYSdqIp0WIuVM24fbSpw5uoZQi8k,426
18
+ vital_agent_container_sdk-0.1.2.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
19
+ vital_agent_container_sdk-0.1.2.dist-info/METADATA,sha256=UqRcZ3v3Q_jkm_2luMEthBtiSGKzeKVDR265f9uSgrY,1187
20
+ vital_agent_container_sdk-0.1.2.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
21
+ vital_agent_container_sdk-0.1.2.dist-info/top_level.txt,sha256=acZrRgjFIj0mWxdLPWONKwMgxnFReQmyjttb8Z76fsc,22
22
+ vital_agent_container_sdk-0.1.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (72.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,19 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: vital-agent-container-sdk
3
- Version: 0.1.0
4
- Summary: Vital Agent Container SDK
5
- Home-page: https://github.com/vital-ai/vital-agent-container-python
6
- Author: Marc Hadfield
7
- Author-email: marc@vital.ai
8
- License: Apache License 2.0
9
- Classifier: Programming Language :: Python :: 3.10
10
- Classifier: License :: OSI Approved :: Apache Software License
11
- Classifier: Operating System :: OS Independent
12
- Requires-Python: >=3.10
13
- Description-Content-Type: text/markdown
14
- License-File: LICENSE
15
- Requires-Dist: vital-ai-vitalsigns >=0.1.19
16
- Requires-Dist: vital-ai-aimp >=0.1.6
17
- Provides-Extra: dev
18
-
19
- # vital-agent-container-python
@@ -1,6 +0,0 @@
1
- vital_agent_container/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- vital_agent_container_sdk-0.1.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
3
- vital_agent_container_sdk-0.1.0.dist-info/METADATA,sha256=hXtZ54QkKEWfPiVd8eBYfx9F2bGF0WpddEw_EPvC1tc,630
4
- vital_agent_container_sdk-0.1.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
5
- vital_agent_container_sdk-0.1.0.dist-info/top_level.txt,sha256=acZrRgjFIj0mWxdLPWONKwMgxnFReQmyjttb8Z76fsc,22
6
- vital_agent_container_sdk-0.1.0.dist-info/RECORD,,