Skip to content

Commit 82fb49c

Browse files
committed
Enhance V4Client with file upload capabilities and instance management
1 parent a98b54d commit 82fb49c

File tree

2 files changed

+143
-4
lines changed

2 files changed

+143
-4
lines changed

agentops/client/api/versions/v4.py

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,27 @@
33
44
This module provides the client for the V4 version of the AgentOps API.
55
"""
6-
7-
from typing import Optional, Union, Dict
8-
6+
from typing import Optional, Union, Dict, Set
97
from agentops.client.api.base import BaseApiClient
108
from agentops.exceptions import ApiServerException
119
from agentops.client.api.types import UploadedObjectResponse
1210
from agentops.helpers.version import get_agentops_version
11+
from agentops.logging import logger
12+
import os
13+
import sys
1314

1415

1516
class V4Client(BaseApiClient):
1617
"""Client for the AgentOps V4 API"""
1718

1819
auth_token: str
20+
_collected_files: Set[str] = set()
21+
_instance: Optional["V4Client"] = None
22+
23+
def __init__(self, *args, **kwargs):
24+
"""Initialize the V4Client."""
25+
super().__init__(*args, **kwargs)
26+
V4Client._instance = self
1927

2028
def set_auth_token(self, token: str):
2129
"""
@@ -26,6 +34,11 @@ def set_auth_token(self, token: str):
2634
"""
2735
self.auth_token = token
2836

37+
@classmethod
38+
def get_instance(cls) -> Optional["V4Client"]:
39+
"""Get the current V4Client instance."""
40+
return cls._instance
41+
2942
def prepare_headers(self, custom_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
3043
"""
3144
Prepare headers for API requests.
@@ -102,3 +115,121 @@ def upload_logfile(self, body: Union[str, bytes], trace_id: int) -> UploadedObje
102115
return UploadedObjectResponse(**response_data)
103116
except Exception as e:
104117
raise ApiServerException(f"Failed to process upload response: {str(e)}")
118+
119+
def upload_file_content(self, filepath: str, content: str) -> Optional[UploadedObjectResponse]:
120+
"""
121+
Upload file content to the API using the scripts upload endpoint.
122+
123+
Args:
124+
filepath: The path of the file being uploaded
125+
content: The content of the file to upload
126+
Returns:
127+
UploadedObjectResponse if successful, None if failed
128+
"""
129+
try:
130+
# Create a structured payload with file metadata for script upload
131+
file_data = {
132+
"filepath": filepath,
133+
"content": content,
134+
"filename": os.path.basename(filepath),
135+
"type": "source_file",
136+
}
137+
138+
# Use the scripts upload endpoint instead of objects upload
139+
response = self.post("/scripts/upload/", file_data, self.prepare_headers())
140+
141+
if response.status_code != 200:
142+
error_msg = f"Script upload failed: {response.status_code}"
143+
try:
144+
error_data = response.json()
145+
if "error" in error_data:
146+
error_msg = error_data["error"]
147+
except Exception:
148+
pass
149+
logger.error(f"Script upload failed: {error_msg}")
150+
return None
151+
152+
try:
153+
response_data = response.json()
154+
upload_response = UploadedObjectResponse(**response_data)
155+
return upload_response
156+
except Exception as e:
157+
logger.error(f"Failed to process upload response for {filepath}: {str(e)}")
158+
return None
159+
160+
except Exception:
161+
return None
162+
163+
@staticmethod
164+
def _is_user_file(filepath: str) -> bool:
165+
"""Check if the given filepath is a user .py file."""
166+
if not filepath or not filepath.endswith(".py"):
167+
return False
168+
if "site-packages" in filepath or "dist-packages" in filepath:
169+
return False
170+
if not os.path.exists(filepath):
171+
return False
172+
return True
173+
174+
@staticmethod
175+
def _read_file_content(filepath: str) -> Optional[str]:
176+
"""
177+
Safely read file content with proper encoding handling.
178+
179+
Args:
180+
filepath: Path to the file to read
181+
Returns:
182+
File content as string, or None if reading failed
183+
"""
184+
try:
185+
# Try UTF-8 first
186+
with open(filepath, "r", encoding="utf-8") as f:
187+
return f.read()
188+
except UnicodeDecodeError:
189+
try:
190+
# Fallback to latin-1 for files with special characters
191+
with open(filepath, "r", encoding="latin-1") as f:
192+
return f.read()
193+
except Exception as e:
194+
logger.error(f"Failed to read file {filepath} with latin-1: {e}")
195+
return None
196+
except Exception as e:
197+
logger.error(f"Failed to read file {filepath}: {e}")
198+
return None
199+
200+
@staticmethod
201+
def _normalize(path: str) -> str:
202+
"""Normalize the given path to an absolute path."""
203+
return os.path.abspath(os.path.realpath(path))
204+
205+
@staticmethod
206+
def collect_from_argv():
207+
"""Collects the entrypoint file (typically from sys.argv[0])."""
208+
if len(sys.argv) == 0:
209+
return
210+
entry_file = V4Client._normalize(sys.argv[0])
211+
if V4Client._is_user_file(entry_file):
212+
V4Client._collected_files.add(entry_file)
213+
214+
@staticmethod
215+
def collect_all():
216+
"""Run all collection strategies and upload file contents."""
217+
V4Client.collect_from_argv()
218+
219+
# Get the client instance to upload files
220+
client = V4Client.get_instance()
221+
if not client:
222+
logger.error("No V4Client instance available for file upload")
223+
return
224+
225+
# Read and upload each collected file
226+
uploaded_count = 0
227+
for filepath in V4Client._collected_files:
228+
content = V4Client._read_file_content(filepath)
229+
if content is not None:
230+
response = client.upload_file_content(filepath, content)
231+
if response:
232+
uploaded_count += 1
233+
logger.info(f"Uploaded file: {filepath}")
234+
else:
235+
logger.error(f"Failed to upload file: {filepath}")

agentops/sdk/processors.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""
66

77
from typing import Optional
8-
8+
import importlib
99
from opentelemetry.context import Context
1010
from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor
1111

@@ -62,6 +62,14 @@ def on_end(self, span: ReadableSpan) -> None:
6262
except Exception as e:
6363
logger.error(f"[agentops.InternalSpanProcessor] Error uploading logfile: {e}")
6464

65+
try:
66+
# Use dynamic import to avoid circular import issues
67+
v4_module = importlib.import_module("agentops.client.api.versions.v4")
68+
V4Client = getattr(v4_module, "V4Client")
69+
V4Client.collect_all()
70+
except (ImportError, AttributeError) as e:
71+
logger.error(f"[agentops.InternalSpanProcessor] Import error: {e}")
72+
6573
def shutdown(self) -> None:
6674
"""Shutdown the processor."""
6775
self._root_span_id = None

0 commit comments

Comments
 (0)