ai-agent-scope 0.2.0__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,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: ai-agent-scope
3
+ Version: 0.2.0
4
+ Summary: Agent debugging and observability platform - SDK
5
+ Home-page: https://github.com/shenchengtsi/agent-scope
6
+ Author: AgentScope Team
7
+ Author-email: agentscope@example.com
8
+ Project-URL: Bug Reports, https://github.com/shenchengtsi/agent-scope/issues
9
+ Project-URL: Source, https://github.com/shenchengtsi/agent-scope
10
+ Project-URL: Documentation, https://github.com/shenchengtsi/agent-scope/blob/main/docs/
11
+ Keywords: agent,debugging,observability,monitoring,llm,ai
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Software Development :: Debuggers
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Operating System :: OS Independent
24
+ Requires-Python: >=3.8
25
+ Description-Content-Type: text/markdown
26
+ Requires-Dist: requests>=2.28.0
27
+ Requires-Dist: pydantic>=2.0.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
30
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
31
+ Requires-Dist: black>=23.0.0; extra == "dev"
32
+ Requires-Dist: flake8>=6.0.0; extra == "dev"
33
+ Requires-Dist: isort>=5.12.0; extra == "dev"
34
+ Dynamic: author
35
+ Dynamic: author-email
36
+ Dynamic: classifier
37
+ Dynamic: description
38
+ Dynamic: description-content-type
39
+ Dynamic: home-page
40
+ Dynamic: keywords
41
+ Dynamic: project-url
42
+ Dynamic: provides-extra
43
+ Dynamic: requires-dist
44
+ Dynamic: requires-python
45
+ Dynamic: summary
46
+
47
+ # AgentScope SDK
48
+
49
+ Python SDK for AgentScope - Agent debugging and observability platform.
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ pip install agentscope
55
+ ```
56
+
57
+ ## Quick Start
58
+
59
+ ```python
60
+ from agentscope import trace, init_monitor
61
+
62
+ # Initialize monitoring
63
+ init_monitor("http://localhost:8000")
64
+
65
+ @trace(name="my_agent")
66
+ def my_agent(query: str):
67
+ # Your agent logic here
68
+ return f"Result for: {query}"
69
+
70
+ # Run your agent
71
+ result = my_agent("What is AI?")
72
+ ```
73
+
74
+ ## Features
75
+
76
+ - **Zero-intrusion**: Just add `@trace` decorator
77
+ - **Real-time monitoring**: WebSocket-based live updates
78
+ - **Execution tracing**: Full chain of thought visualization
79
+ - **Tool call tracking**: Debug function calling issues
80
+ - **Token & latency metrics**: Performance monitoring
@@ -0,0 +1,34 @@
1
+ # AgentScope SDK
2
+
3
+ Python SDK for AgentScope - Agent debugging and observability platform.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install agentscope
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from agentscope import trace, init_monitor
15
+
16
+ # Initialize monitoring
17
+ init_monitor("http://localhost:8000")
18
+
19
+ @trace(name="my_agent")
20
+ def my_agent(query: str):
21
+ # Your agent logic here
22
+ return f"Result for: {query}"
23
+
24
+ # Run your agent
25
+ result = my_agent("What is AI?")
26
+ ```
27
+
28
+ ## Features
29
+
30
+ - **Zero-intrusion**: Just add `@trace` decorator
31
+ - **Real-time monitoring**: WebSocket-based live updates
32
+ - **Execution tracing**: Full chain of thought visualization
33
+ - **Tool call tracking**: Debug function calling issues
34
+ - **Token & latency metrics**: Performance monitoring
@@ -0,0 +1,43 @@
1
+ """AgentScope - Agent debugging and observability platform."""
2
+
3
+ from .monitor import (
4
+ init_monitor,
5
+ trace,
6
+ trace_scope,
7
+ instrument_llm,
8
+ instrument_openai,
9
+ instrumented_tool,
10
+ get_current_trace,
11
+ add_step,
12
+ add_llm_call,
13
+ add_tool_call,
14
+ add_thinking,
15
+ add_memory,
16
+ )
17
+ from .models import TraceEvent, ExecutionStep, ToolCall, StepType, Status
18
+
19
+ __version__ = "0.2.0"
20
+ __all__ = [
21
+ # Core initialization
22
+ "init_monitor",
23
+ # Tracing APIs
24
+ "trace",
25
+ "trace_scope",
26
+ # Auto-instrumentation
27
+ "instrument_llm",
28
+ "instrument_openai",
29
+ "instrumented_tool",
30
+ # Utilities
31
+ "get_current_trace",
32
+ "add_step",
33
+ "add_llm_call",
34
+ "add_tool_call",
35
+ "add_thinking",
36
+ "add_memory",
37
+ # Models
38
+ "TraceEvent",
39
+ "ExecutionStep",
40
+ "ToolCall",
41
+ "StepType",
42
+ "Status",
43
+ ]
@@ -0,0 +1,126 @@
1
+ """Data models for AgentScope."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Optional, List, Dict
5
+ from datetime import datetime
6
+ from enum import Enum
7
+ import uuid
8
+ import json
9
+
10
+
11
+ class StepType(str, Enum):
12
+ """Types of execution steps."""
13
+ INPUT = "input"
14
+ LLM_CALL = "llm_call"
15
+ TOOL_CALL = "tool_call"
16
+ TOOL_RESULT = "tool_result"
17
+ OUTPUT = "output"
18
+ ERROR = "error"
19
+ THINKING = "thinking"
20
+
21
+
22
+ class Status(str, Enum):
23
+ """Execution status."""
24
+ PENDING = "pending"
25
+ RUNNING = "running"
26
+ SUCCESS = "success"
27
+ ERROR = "error"
28
+
29
+
30
+ @dataclass
31
+ class ToolCall:
32
+ """Represents a tool/function call."""
33
+ id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
34
+ name: str = ""
35
+ arguments: Dict[str, Any] = field(default_factory=dict)
36
+ result: Any = None
37
+ error: Optional[str] = None
38
+ latency_ms: float = 0.0
39
+
40
+ def to_dict(self) -> Dict:
41
+ return {
42
+ "id": self.id,
43
+ "name": self.name,
44
+ "arguments": self.arguments,
45
+ "result": self.result,
46
+ "error": self.error,
47
+ "latency_ms": self.latency_ms,
48
+ }
49
+
50
+
51
+ @dataclass
52
+ class ExecutionStep:
53
+ """A single step in the execution chain."""
54
+ id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
55
+ type: StepType = StepType.INPUT
56
+ content: str = ""
57
+ timestamp: datetime = field(default_factory=datetime.utcnow)
58
+ tokens_input: int = 0
59
+ tokens_output: int = 0
60
+ latency_ms: float = 0.0
61
+ tool_call: Optional[ToolCall] = None
62
+ metadata: Dict[str, Any] = field(default_factory=dict)
63
+ status: Status = Status.PENDING
64
+
65
+ def to_dict(self) -> Dict:
66
+ return {
67
+ "id": self.id,
68
+ "type": self.type.value,
69
+ "content": self.content,
70
+ "timestamp": self.timestamp.isoformat(),
71
+ "tokens_input": self.tokens_input,
72
+ "tokens_output": self.tokens_output,
73
+ "latency_ms": self.latency_ms,
74
+ "tool_call": self.tool_call.to_dict() if self.tool_call else None,
75
+ "metadata": self.metadata,
76
+ "status": self.status.value,
77
+ }
78
+
79
+
80
+ @dataclass
81
+ class TraceEvent:
82
+ """A complete trace of an Agent execution."""
83
+ id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
84
+ name: str = ""
85
+ tags: List[str] = field(default_factory=list)
86
+ start_time: datetime = field(default_factory=datetime.utcnow)
87
+ end_time: Optional[datetime] = None
88
+ steps: List[ExecutionStep] = field(default_factory=list)
89
+ status: Status = Status.PENDING
90
+ total_tokens: int = 0
91
+ total_latency_ms: float = 0.0
92
+ input_query: str = ""
93
+ output_result: str = ""
94
+ metadata: Dict[str, Any] = field(default_factory=dict)
95
+
96
+ def add_step(self, step: ExecutionStep):
97
+ """Add a step to the trace."""
98
+ self.steps.append(step)
99
+ self.total_tokens += step.tokens_input + step.tokens_output
100
+
101
+ def finish(self, status: Status = Status.SUCCESS):
102
+ """Mark the trace as finished."""
103
+ self.end_time = datetime.utcnow()
104
+ self.status = status
105
+ if self.start_time:
106
+ self.total_latency_ms = (self.end_time - self.start_time).total_seconds() * 1000
107
+
108
+ def to_dict(self) -> Dict:
109
+ return {
110
+ "id": self.id,
111
+ "name": self.name,
112
+ "tags": self.tags,
113
+ "start_time": self.start_time.isoformat() if self.start_time else None,
114
+ "end_time": self.end_time.isoformat() if self.end_time else None,
115
+ "steps": [s.to_dict() for s in self.steps],
116
+ "status": self.status.value,
117
+ "total_tokens": self.total_tokens,
118
+ "total_latency_ms": self.total_latency_ms,
119
+ "input_query": self.input_query,
120
+ "output_result": self.output_result,
121
+ "metadata": self.metadata,
122
+ }
123
+
124
+ def to_json(self) -> str:
125
+ """Convert to JSON string."""
126
+ return json.dumps(self.to_dict(), ensure_ascii=False)
@@ -0,0 +1,537 @@
1
+ """Core monitoring functionality for AgentScope - Scheme 3 Implementation.
2
+
3
+ This module provides deep monitoring with low intrusion using:
4
+ 1. Context Manager pattern (trace_scope)
5
+ 2. ContextVars for context propagation
6
+ 3. Auto-instrumentation for LLM clients and tools
7
+ """
8
+
9
+ import functools
10
+ import requests
11
+ import threading
12
+ import logging
13
+ import time
14
+ import json
15
+ from typing import Optional, List, Callable, Any, Dict
16
+ from contextvars import ContextVar
17
+ from contextlib import contextmanager
18
+ from datetime import datetime
19
+
20
+ from .models import TraceEvent, ExecutionStep, ToolCall, StepType, Status
21
+
22
+ # Global configuration
23
+ _monitor_url: Optional[str] = None
24
+ _current_trace: ContextVar[Optional[TraceEvent]] = ContextVar('current_trace', default=None)
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # Track which clients have been instrumented (to avoid double wrapping)
29
+ _instrumented_clients: set = set()
30
+ _original_openai_create: Optional[Callable] = None
31
+
32
+
33
+ def init_monitor(url: str = "http://localhost:8000"):
34
+ """Initialize the AgentScope monitor.
35
+
36
+ Args:
37
+ url: The URL of the AgentScope backend server
38
+ """
39
+ global _monitor_url
40
+ _monitor_url = url.rstrip('/')
41
+ logger.info(f"AgentScope monitor initialized: {_monitor_url}")
42
+
43
+
44
+ def get_current_trace() -> Optional[TraceEvent]:
45
+ """Get the current trace in context."""
46
+ return _current_trace.get()
47
+
48
+
49
+ def set_current_trace(trace: Optional[TraceEvent]):
50
+ """Set the current trace in context."""
51
+ _current_trace.set(trace)
52
+
53
+
54
+ def _send_trace(trace: TraceEvent):
55
+ """Send trace to the backend server."""
56
+ global _monitor_url
57
+ if not _monitor_url:
58
+ return
59
+
60
+ try:
61
+ response = requests.post(
62
+ f"{_monitor_url}/api/traces",
63
+ json=trace.to_dict(),
64
+ timeout=5
65
+ )
66
+ if response.status_code != 200:
67
+ logger.warning(f"Failed to send trace: {response.status_code}")
68
+ try:
69
+ error_detail = response.json()
70
+ logger.warning(f"Error detail: {error_detail}")
71
+ except:
72
+ logger.warning(f"Response text: {response.text[:200]}")
73
+ except Exception as e:
74
+ logger.warning(f"Failed to send trace: {e}")
75
+
76
+
77
+ # =============================================================================
78
+ # Scheme 3: Context Manager Pattern (trace_scope)
79
+ # =============================================================================
80
+
81
+ @contextmanager
82
+ def trace_scope(
83
+ name: str,
84
+ input_query: str = "",
85
+ tags: Optional[List[str]] = None,
86
+ metadata: Optional[Dict[str, Any]] = None
87
+ ):
88
+ """Context manager for tracing Agent execution.
89
+
90
+ This is the core of Scheme 3 - creating a "tracing bubble" where all
91
+ LLM calls, tool executions, and memory operations are automatically recorded.
92
+
93
+ Args:
94
+ name: Name of the trace
95
+ input_query: The input query/prompt
96
+ tags: List of tags for categorization
97
+ metadata: Additional metadata
98
+
99
+ Example:
100
+ with trace_scope("my_agent", input_query="Hello"):
101
+ result = llm.chat("Hello") # Auto-recorded
102
+ tool_result = search("query") # Auto-recorded
103
+ return result
104
+ """
105
+ trace_event = TraceEvent(
106
+ name=name,
107
+ tags=tags or [],
108
+ input_query=input_query,
109
+ )
110
+
111
+ if metadata:
112
+ trace_event.metadata = metadata
113
+
114
+ # Set as current trace in context
115
+ token = _current_trace.set(trace_event)
116
+
117
+ # Add input step
118
+ if input_query:
119
+ input_step = ExecutionStep(
120
+ type=StepType.INPUT,
121
+ content=input_query[:1000],
122
+ status=Status.SUCCESS,
123
+ )
124
+ trace_event.add_step(input_step)
125
+
126
+ logger.debug(f"AgentScope: Started trace {trace_event.id} for {name}")
127
+
128
+ try:
129
+ yield trace_event
130
+ trace_event.finish(Status.SUCCESS)
131
+ logger.debug(f"AgentScope: Trace {trace_event.id} finished successfully")
132
+ except Exception as e:
133
+ # Add error step
134
+ error_step = ExecutionStep(
135
+ type=StepType.ERROR,
136
+ content=str(e)[:500],
137
+ status=Status.ERROR,
138
+ )
139
+ trace_event.add_step(error_step)
140
+ trace_event.finish(Status.ERROR)
141
+ logger.debug(f"AgentScope: Trace {trace_event.id} finished with error: {e}")
142
+ raise
143
+ finally:
144
+ # Send trace to backend
145
+ _send_trace(trace_event)
146
+ # Clear context
147
+ _current_trace.reset(token)
148
+
149
+
150
+ # =============================================================================
151
+ # Auto-Instrumentation: LLM Clients
152
+ # =============================================================================
153
+
154
+ def _wrap_openai_chat_completion(original_create):
155
+ """Wrap OpenAI's chat.completions.create method to auto-trace LLM calls."""
156
+
157
+ @functools.wraps(original_create)
158
+ def wrapped_create(self, *args, **kwargs):
159
+ trace = get_current_trace()
160
+ if not trace:
161
+ # No active trace, just call original
162
+ return original_create(self, *args, **kwargs)
163
+
164
+ # Extract information for tracing
165
+ model = kwargs.get('model', 'unknown')
166
+ messages = kwargs.get('messages', [])
167
+
168
+ # Build prompt preview
169
+ prompt_preview = ""
170
+ if messages:
171
+ last_msg = messages[-1] if isinstance(messages, list) else messages
172
+ if isinstance(last_msg, dict):
173
+ prompt_preview = last_msg.get('content', '')[:200]
174
+ else:
175
+ prompt_preview = str(last_msg)[:200]
176
+
177
+ start_time = time.time()
178
+ tokens_input = 0
179
+ tokens_output = 0
180
+
181
+ try:
182
+ # Call original method
183
+ result = original_create(self, *args, **kwargs)
184
+
185
+ # Calculate latency
186
+ latency_ms = (time.time() - start_time) * 1000
187
+
188
+ # Extract token usage if available
189
+ if hasattr(result, 'usage') and result.usage:
190
+ tokens_input = getattr(result.usage, 'prompt_tokens', 0)
191
+ tokens_output = getattr(result.usage, 'completion_tokens', 0)
192
+
193
+ # Extract completion content
194
+ completion_preview = ""
195
+ if hasattr(result, 'choices') and result.choices:
196
+ choice = result.choices[0]
197
+ if hasattr(choice, 'message') and choice.message:
198
+ completion_preview = getattr(choice.message, 'content', '')[:200]
199
+ elif hasattr(choice, 'text'):
200
+ completion_preview = choice.text[:200]
201
+
202
+ # Add LLM call step to trace
203
+ step = ExecutionStep(
204
+ type=StepType.LLM_CALL,
205
+ content=f"Model: {model}\nPrompt: {prompt_preview}...\nCompletion: {completion_preview}...",
206
+ tokens_input=tokens_input,
207
+ tokens_output=tokens_output,
208
+ latency_ms=latency_ms,
209
+ metadata={
210
+ 'model': model,
211
+ 'messages_count': len(messages) if isinstance(messages, list) else 1,
212
+ 'prompt_preview': prompt_preview,
213
+ 'completion_preview': completion_preview,
214
+ },
215
+ status=Status.SUCCESS,
216
+ )
217
+ trace.add_step(step)
218
+ logger.debug(f"AgentScope: Recorded LLM call to {model} ({latency_ms:.1f}ms)")
219
+
220
+ return result
221
+
222
+ except Exception as e:
223
+ # Record error
224
+ latency_ms = (time.time() - start_time) * 1000
225
+ step = ExecutionStep(
226
+ type=StepType.LLM_CALL,
227
+ content=f"Model: {model}\nError: {str(e)}",
228
+ latency_ms=latency_ms,
229
+ metadata={'model': model, 'error': str(e)},
230
+ status=Status.ERROR,
231
+ )
232
+ trace.add_step(step)
233
+ raise
234
+
235
+ return wrapped_create
236
+
237
+
238
+ def instrument_llm(client: Any) -> Any:
239
+ """Instrument an LLM client to auto-trace all calls.
240
+
241
+ Currently supports:
242
+ - OpenAI client (openai.OpenAI)
243
+ - OpenAI Async client (openai.AsyncOpenAI)
244
+
245
+ Args:
246
+ client: The LLM client instance
247
+
248
+ Returns:
249
+ The instrumented client (same instance, methods wrapped)
250
+
251
+ Example:
252
+ import openai
253
+ client = instrument_llm(openai.OpenAI())
254
+
255
+ with trace_scope("my_agent"):
256
+ # This call will be automatically traced
257
+ response = client.chat.completions.create(...)
258
+ """
259
+ global _instrumented_clients, _original_openai_create
260
+
261
+ # Check if already instrumented
262
+ client_id = id(client)
263
+ if client_id in _instrumented_clients:
264
+ return client
265
+
266
+ # Try to detect client type and instrument
267
+ client_class = client.__class__.__name__
268
+ module_name = getattr(client.__class__, '__module__', '')
269
+
270
+ try:
271
+ if 'openai' in module_name.lower():
272
+ # OpenAI client
273
+ if hasattr(client, 'chat') and hasattr(client.chat, 'completions'):
274
+ orig_create = client.chat.completions.create
275
+ if not hasattr(orig_create, '_agentscope_wrapped'):
276
+ client.chat.completions.create = _wrap_openai_chat_completion(orig_create)
277
+ client.chat.completions.create._agentscope_wrapped = True
278
+ _original_openai_create = orig_create
279
+ logger.info(f"AgentScope: Instrumented {client_class}")
280
+
281
+ _instrumented_clients.add(client_id)
282
+
283
+ except Exception as e:
284
+ logger.warning(f"AgentScope: Failed to instrument client: {e}")
285
+
286
+ return client
287
+
288
+
289
+ def instrument_openai():
290
+ """Globally instrument the OpenAI module.
291
+
292
+ This patches the OpenAI class so all new instances are automatically
293
+ instrumented.
294
+
295
+ Example:
296
+ instrument_openai()
297
+
298
+ # All OpenAI clients created after this will be auto-traced
299
+ client = openai.OpenAI()
300
+ """
301
+ try:
302
+ import openai
303
+
304
+ original_init = openai.OpenAI.__init__
305
+
306
+ @functools.wraps(original_init)
307
+ def patched_init(self, *args, **kwargs):
308
+ original_init(self, *args, **kwargs)
309
+ # Auto-instrument this instance
310
+ instrument_llm(self)
311
+
312
+ openai.OpenAI.__init__ = patched_init
313
+ logger.info("AgentScope: OpenAI module instrumented globally")
314
+
315
+ except ImportError:
316
+ logger.warning("AgentScope: OpenAI not installed, skipping global instrumentation")
317
+ except Exception as e:
318
+ logger.warning(f"AgentScope: Failed to instrument OpenAI: {e}")
319
+
320
+
321
+ # =============================================================================
322
+ # Auto-Instrumentation: Tools
323
+ # =============================================================================
324
+
325
+ def instrumented_tool(func: Optional[Callable] = None, *, name: Optional[str] = None):
326
+ """Decorator to auto-trace tool function calls.
327
+
328
+ Args:
329
+ func: The function to decorate (when used without parentheses)
330
+ name: Optional custom name for the tool (defaults to function name)
331
+
332
+ Returns:
333
+ Decorated function that auto-traces its execution
334
+
335
+ Example:
336
+ @instrumented_tool
337
+ def search(query: str) -> str:
338
+ return requests.get(f"https://api.com?q={query}").text
339
+
340
+ @instrumented_tool(name="weather_api")
341
+ def get_weather(city: str) -> dict:
342
+ return {"temp": 25, "weather": "sunny"}
343
+
344
+ with trace_scope("agent"):
345
+ result = search("python") # Auto-traced
346
+ """
347
+ def decorator(f: Callable) -> Callable:
348
+ tool_name = name or f.__name__
349
+
350
+ @functools.wraps(f)
351
+ def wrapper(*args, **kwargs):
352
+ trace = get_current_trace()
353
+ if not trace:
354
+ # No active trace, just call original
355
+ return f(*args, **kwargs)
356
+
357
+ # Build arguments dict
358
+ arguments = {}
359
+ if args:
360
+ arguments['args'] = [str(a)[:100] for a in args]
361
+ if kwargs:
362
+ arguments['kwargs'] = {k: str(v)[:100] for k, v in kwargs.items()}
363
+
364
+ start_time = time.time()
365
+ error_msg = None
366
+ result = None
367
+ success = False
368
+
369
+ try:
370
+ result = f(*args, **kwargs)
371
+ success = True
372
+ return result
373
+ except Exception as e:
374
+ error_msg = str(e)
375
+ raise
376
+ finally:
377
+ latency_ms = (time.time() - start_time) * 1000
378
+
379
+ # Format result for recording (handle None as valid return value)
380
+ result_str = None
381
+ if success:
382
+ try:
383
+ result_str = json.dumps(result, ensure_ascii=False)[:500] if result is not None else "null"
384
+ except (TypeError, ValueError):
385
+ result_str = str(result)[:500]
386
+
387
+ # Add tool call step
388
+ tool_call = ToolCall(
389
+ name=tool_name,
390
+ arguments=arguments,
391
+ result=result_str,
392
+ error=error_msg,
393
+ latency_ms=latency_ms,
394
+ )
395
+
396
+ step = ExecutionStep(
397
+ type=StepType.TOOL_CALL,
398
+ content=f"Tool: {tool_name}\nArgs: {json.dumps(arguments, ensure_ascii=False)[:200]}",
399
+ tool_call=tool_call,
400
+ latency_ms=latency_ms,
401
+ status=Status.ERROR if error_msg else Status.SUCCESS,
402
+ )
403
+ trace.add_step(step)
404
+ logger.debug(f"AgentScope: Recorded tool call {tool_name} ({latency_ms:.1f}ms)")
405
+
406
+ return wrapper
407
+
408
+ if func is not None:
409
+ # Used without parentheses: @instrumented_tool
410
+ return decorator(func)
411
+ else:
412
+ # Used with parentheses: @instrumented_tool(name="...")
413
+ return decorator
414
+
415
+
416
+ # =============================================================================
417
+ # Legacy Decorator (for backward compatibility)
418
+ # =============================================================================
419
+
420
+ def trace(name: Optional[str] = None, tags: Optional[List[str]] = None):
421
+ """Decorator to trace an Agent function (legacy API).
422
+
423
+ This is kept for backward compatibility. New code should use trace_scope().
424
+
425
+ Args:
426
+ name: Name of the trace (defaults to function name)
427
+ tags: List of tags for categorization
428
+
429
+ Example:
430
+ @trace(name="my_agent", tags=["production"])
431
+ def my_agent(query: str):
432
+ return llm.complete(query)
433
+ """
434
+ def decorator(func: Callable) -> Callable:
435
+ @functools.wraps(func)
436
+ def wrapper(*args, **kwargs):
437
+ input_content = str(args[0]) if args else str(kwargs)
438
+
439
+ with trace_scope(
440
+ name=name or func.__name__,
441
+ input_query=input_content,
442
+ tags=tags
443
+ ):
444
+ return func(*args, **kwargs)
445
+
446
+ return wrapper
447
+ return decorator
448
+
449
+
450
+ # =============================================================================
451
+ # Utility functions for manual tracing
452
+ # =============================================================================
453
+
454
+ def add_step(
455
+ step_type: StepType,
456
+ content: str,
457
+ tokens_input: int = 0,
458
+ tokens_output: int = 0,
459
+ latency_ms: float = 0.0,
460
+ metadata: Optional[Dict[str, Any]] = None,
461
+ ):
462
+ """Add a step to the current trace."""
463
+ trace = get_current_trace()
464
+ if not trace:
465
+ return
466
+
467
+ step = ExecutionStep(
468
+ type=step_type,
469
+ content=content,
470
+ tokens_input=tokens_input,
471
+ tokens_output=tokens_output,
472
+ latency_ms=latency_ms,
473
+ metadata=metadata or {},
474
+ status=Status.SUCCESS,
475
+ )
476
+ trace.add_step(step)
477
+
478
+
479
+ def add_llm_call(
480
+ prompt: str,
481
+ completion: str,
482
+ tokens_input: int = 0,
483
+ tokens_output: int = 0,
484
+ latency_ms: float = 0.0,
485
+ ):
486
+ """Manually record an LLM call step."""
487
+ add_step(
488
+ step_type=StepType.LLM_CALL,
489
+ content=f"Prompt: {prompt[:200]}...\nCompletion: {completion[:200]}...",
490
+ tokens_input=tokens_input,
491
+ tokens_output=tokens_output,
492
+ latency_ms=latency_ms,
493
+ metadata={"prompt": prompt, "completion": completion},
494
+ )
495
+
496
+
497
+ def add_tool_call(
498
+ tool_name: str,
499
+ arguments: dict,
500
+ result: Any,
501
+ error: Optional[str] = None,
502
+ latency_ms: float = 0.0,
503
+ ):
504
+ """Manually record a tool call step."""
505
+ trace = get_current_trace()
506
+ if not trace:
507
+ return
508
+
509
+ tool_call = ToolCall(
510
+ name=tool_name,
511
+ arguments=arguments,
512
+ result=result,
513
+ error=error,
514
+ latency_ms=latency_ms,
515
+ )
516
+
517
+ step = ExecutionStep(
518
+ type=StepType.TOOL_CALL,
519
+ content=f"Tool: {tool_name}",
520
+ tool_call=tool_call,
521
+ latency_ms=latency_ms,
522
+ status=Status.ERROR if error else Status.SUCCESS,
523
+ )
524
+ trace.add_step(step)
525
+
526
+
527
+ def add_thinking(content: str):
528
+ """Record a thinking/reasoning step."""
529
+ add_step(StepType.THINKING, content)
530
+
531
+
532
+ def add_memory(action: str, details: str):
533
+ """Record a memory operation step."""
534
+ add_step(
535
+ step_type=StepType.THINKING,
536
+ content=f"Memory {action}: {details}",
537
+ )
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: ai-agent-scope
3
+ Version: 0.2.0
4
+ Summary: Agent debugging and observability platform - SDK
5
+ Home-page: https://github.com/shenchengtsi/agent-scope
6
+ Author: AgentScope Team
7
+ Author-email: agentscope@example.com
8
+ Project-URL: Bug Reports, https://github.com/shenchengtsi/agent-scope/issues
9
+ Project-URL: Source, https://github.com/shenchengtsi/agent-scope
10
+ Project-URL: Documentation, https://github.com/shenchengtsi/agent-scope/blob/main/docs/
11
+ Keywords: agent,debugging,observability,monitoring,llm,ai
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Software Development :: Debuggers
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Operating System :: OS Independent
24
+ Requires-Python: >=3.8
25
+ Description-Content-Type: text/markdown
26
+ Requires-Dist: requests>=2.28.0
27
+ Requires-Dist: pydantic>=2.0.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
30
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
31
+ Requires-Dist: black>=23.0.0; extra == "dev"
32
+ Requires-Dist: flake8>=6.0.0; extra == "dev"
33
+ Requires-Dist: isort>=5.12.0; extra == "dev"
34
+ Dynamic: author
35
+ Dynamic: author-email
36
+ Dynamic: classifier
37
+ Dynamic: description
38
+ Dynamic: description-content-type
39
+ Dynamic: home-page
40
+ Dynamic: keywords
41
+ Dynamic: project-url
42
+ Dynamic: provides-extra
43
+ Dynamic: requires-dist
44
+ Dynamic: requires-python
45
+ Dynamic: summary
46
+
47
+ # AgentScope SDK
48
+
49
+ Python SDK for AgentScope - Agent debugging and observability platform.
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ pip install agentscope
55
+ ```
56
+
57
+ ## Quick Start
58
+
59
+ ```python
60
+ from agentscope import trace, init_monitor
61
+
62
+ # Initialize monitoring
63
+ init_monitor("http://localhost:8000")
64
+
65
+ @trace(name="my_agent")
66
+ def my_agent(query: str):
67
+ # Your agent logic here
68
+ return f"Result for: {query}"
69
+
70
+ # Run your agent
71
+ result = my_agent("What is AI?")
72
+ ```
73
+
74
+ ## Features
75
+
76
+ - **Zero-intrusion**: Just add `@trace` decorator
77
+ - **Real-time monitoring**: WebSocket-based live updates
78
+ - **Execution tracing**: Full chain of thought visualization
79
+ - **Tool call tracking**: Debug function calling issues
80
+ - **Token & latency metrics**: Performance monitoring
@@ -0,0 +1,13 @@
1
+ README.md
2
+ setup.py
3
+ agentscope/__init__.py
4
+ agentscope/models.py
5
+ agentscope/monitor.py
6
+ ai_agent_scope.egg-info/PKG-INFO
7
+ ai_agent_scope.egg-info/SOURCES.txt
8
+ ai_agent_scope.egg-info/dependency_links.txt
9
+ ai_agent_scope.egg-info/requires.txt
10
+ ai_agent_scope.egg-info/top_level.txt
11
+ tests/__init__.py
12
+ tests/test_models.py
13
+ tests/test_monitor.py
@@ -0,0 +1,9 @@
1
+ requests>=2.28.0
2
+ pydantic>=2.0.0
3
+
4
+ [dev]
5
+ pytest>=7.0.0
6
+ pytest-cov>=4.0.0
7
+ black>=23.0.0
8
+ flake8>=6.0.0
9
+ isort>=5.12.0
@@ -0,0 +1,2 @@
1
+ agentscope
2
+ tests
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,50 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ with open("README.md", "r", encoding="utf-8") as fh:
4
+ long_description = fh.read()
5
+
6
+ setup(
7
+ name="ai-agent-scope",
8
+ version="0.2.0",
9
+ packages=find_packages(),
10
+ install_requires=[
11
+ "requests>=2.28.0",
12
+ "pydantic>=2.0.0",
13
+ ],
14
+ extras_require={
15
+ "dev": [
16
+ "pytest>=7.0.0",
17
+ "pytest-cov>=4.0.0",
18
+ "black>=23.0.0",
19
+ "flake8>=6.0.0",
20
+ "isort>=5.12.0",
21
+ ],
22
+ },
23
+ python_requires=">=3.8",
24
+ author="AgentScope Team",
25
+ author_email="agentscope@example.com",
26
+ description="Agent debugging and observability platform - SDK",
27
+ long_description=long_description,
28
+ long_description_content_type="text/markdown",
29
+ url="https://github.com/shenchengtsi/agent-scope",
30
+ project_urls={
31
+ "Bug Reports": "https://github.com/shenchengtsi/agent-scope/issues",
32
+ "Source": "https://github.com/shenchengtsi/agent-scope",
33
+ "Documentation": "https://github.com/shenchengtsi/agent-scope/blob/main/docs/",
34
+ },
35
+ classifiers=[
36
+ "Development Status :: 3 - Alpha",
37
+ "Intended Audience :: Developers",
38
+ "Topic :: Software Development :: Debuggers",
39
+ "Topic :: Software Development :: Libraries :: Python Modules",
40
+ "License :: OSI Approved :: MIT License",
41
+ "Programming Language :: Python :: 3",
42
+ "Programming Language :: Python :: 3.8",
43
+ "Programming Language :: Python :: 3.9",
44
+ "Programming Language :: Python :: 3.10",
45
+ "Programming Language :: Python :: 3.11",
46
+ "Programming Language :: Python :: 3.12",
47
+ "Operating System :: OS Independent",
48
+ ],
49
+ keywords="agent, debugging, observability, monitoring, llm, ai",
50
+ )
@@ -0,0 +1 @@
1
+ # AgentScope SDK Tests
@@ -0,0 +1,158 @@
1
+ """Tests for AgentScope data models."""
2
+
3
+ import pytest
4
+ from datetime import datetime
5
+
6
+ from agentscope.models import (
7
+ ToolCall,
8
+ ExecutionStep,
9
+ TraceEvent,
10
+ StepType,
11
+ Status,
12
+ )
13
+
14
+
15
+ class TestToolCall:
16
+ """Test ToolCall model."""
17
+
18
+ def test_tool_call_creation(self):
19
+ """Test creating a ToolCall."""
20
+ tool_call = ToolCall(
21
+ name="search",
22
+ arguments={"query": "python"},
23
+ result="Python is great",
24
+ latency_ms=100.0,
25
+ )
26
+
27
+ assert tool_call.name == "search"
28
+ assert tool_call.arguments == {"query": "python"}
29
+ assert tool_call.result == "Python is great"
30
+ assert tool_call.latency_ms == 100.0
31
+ assert tool_call.id is not None
32
+
33
+ def test_tool_call_to_dict(self):
34
+ """Test ToolCall serialization."""
35
+ tool_call = ToolCall(
36
+ name="search",
37
+ arguments={"query": "python"},
38
+ result="result",
39
+ error=None,
40
+ latency_ms=100.0,
41
+ )
42
+
43
+ d = tool_call.to_dict()
44
+ assert d["name"] == "search"
45
+ assert d["arguments"] == {"query": "python"}
46
+ assert d["result"] == "result"
47
+ assert d["error"] is None
48
+
49
+
50
+ class TestExecutionStep:
51
+ """Test ExecutionStep model."""
52
+
53
+ def test_step_creation(self):
54
+ """Test creating an ExecutionStep."""
55
+ step = ExecutionStep(
56
+ type=StepType.LLM_CALL,
57
+ content="Test content",
58
+ tokens_input=100,
59
+ tokens_output=50,
60
+ latency_ms=500.0,
61
+ status=Status.SUCCESS,
62
+ )
63
+
64
+ assert step.type == StepType.LLM_CALL
65
+ assert step.content == "Test content"
66
+ assert step.tokens_input == 100
67
+ assert step.tokens_output == 50
68
+ assert step.latency_ms == 500.0
69
+ assert step.status == Status.SUCCESS
70
+
71
+ def test_step_to_dict(self):
72
+ """Test ExecutionStep serialization."""
73
+ step = ExecutionStep(
74
+ type=StepType.TOOL_CALL,
75
+ content="Tool execution",
76
+ status=Status.SUCCESS,
77
+ )
78
+
79
+ d = step.to_dict()
80
+ assert d["type"] == "tool_call"
81
+ assert d["content"] == "Tool execution"
82
+ assert d["status"] == "success"
83
+ assert "timestamp" in d
84
+
85
+
86
+ class TestTraceEvent:
87
+ """Test TraceEvent model."""
88
+
89
+ def test_trace_creation(self):
90
+ """Test creating a TraceEvent."""
91
+ trace = TraceEvent(
92
+ name="test_trace",
93
+ tags=["test", "debug"],
94
+ input_query="test query",
95
+ )
96
+
97
+ assert trace.name == "test_trace"
98
+ assert trace.tags == ["test", "debug"]
99
+ assert trace.input_query == "test query"
100
+ assert trace.status == Status.PENDING
101
+ assert trace.total_tokens == 0
102
+
103
+ def test_add_step(self):
104
+ """Test adding steps to trace."""
105
+ trace = TraceEvent(name="test")
106
+ step = ExecutionStep(type=StepType.INPUT, content="Input")
107
+
108
+ trace.add_step(step)
109
+
110
+ assert len(trace.steps) == 1
111
+ assert trace.total_tokens == step.tokens_input + step.tokens_output
112
+
113
+ def test_finish_success(self):
114
+ """Test finishing trace with success."""
115
+ trace = TraceEvent(name="test")
116
+ trace.add_step(ExecutionStep(
117
+ type=StepType.INPUT,
118
+ content="Input",
119
+ tokens_input=10,
120
+ tokens_output=5,
121
+ ))
122
+
123
+ trace.finish(Status.SUCCESS)
124
+
125
+ assert trace.status == Status.SUCCESS
126
+ assert trace.end_time is not None
127
+ assert trace.total_latency_ms > 0
128
+
129
+ def test_finish_error(self):
130
+ """Test finishing trace with error."""
131
+ trace = TraceEvent(name="test")
132
+ trace.finish(Status.ERROR)
133
+
134
+ assert trace.status == Status.ERROR
135
+ assert trace.end_time is not None
136
+
137
+ def test_to_dict(self):
138
+ """Test TraceEvent serialization."""
139
+ trace = TraceEvent(
140
+ name="test",
141
+ tags=["debug"],
142
+ input_query="query",
143
+ metadata={"key": "value"},
144
+ )
145
+ trace.add_step(ExecutionStep(type=StepType.INPUT, content="Input"))
146
+ trace.finish(Status.SUCCESS)
147
+
148
+ d = trace.to_dict()
149
+ assert d["name"] == "test"
150
+ assert d["tags"] == ["debug"]
151
+ assert d["input_query"] == "query"
152
+ assert d["metadata"] == {"key": "value"}
153
+ assert d["status"] == "success"
154
+ assert len(d["steps"]) == 1
155
+
156
+
157
+ if __name__ == "__main__":
158
+ pytest.main([__file__, "-v"])
@@ -0,0 +1,140 @@
1
+ """Tests for AgentScope monitoring functionality."""
2
+
3
+ import pytest
4
+ import time
5
+ from unittest.mock import Mock, patch
6
+
7
+ from agentscope import (
8
+ trace_scope,
9
+ get_current_trace,
10
+ add_thinking,
11
+ add_llm_call,
12
+ add_tool_call,
13
+ init_monitor,
14
+ )
15
+ from agentscope.models import StepType, Status
16
+
17
+
18
+ class TestTraceScope:
19
+ """Test trace_scope context manager."""
20
+
21
+ def test_trace_scope_creates_trace(self):
22
+ """Test that trace_scope creates a trace event."""
23
+ with trace_scope("test_trace", input_query="test input") as trace:
24
+ assert trace is not None
25
+ assert trace.name == "test_trace"
26
+ assert trace.input_query == "test input"
27
+
28
+ def test_trace_scope_adds_input_step(self):
29
+ """Test that trace_scope adds an input step."""
30
+ with trace_scope("test_trace", input_query="test input") as trace:
31
+ pass
32
+
33
+ # After exiting, trace should have input step
34
+ assert len(trace.steps) >= 1
35
+ assert trace.steps[0].type == StepType.INPUT
36
+
37
+ def test_trace_scope_handles_exception(self):
38
+ """Test that trace_scope handles exceptions properly."""
39
+ trace = None
40
+ try:
41
+ with trace_scope("test_trace", input_query="test") as t:
42
+ trace = t
43
+ raise ValueError("Test error")
44
+ except ValueError:
45
+ pass
46
+
47
+ # Trace should have error step
48
+ assert trace is not None
49
+ assert trace.status == Status.ERROR
50
+ error_steps = [s for s in trace.steps if s.type == StepType.ERROR]
51
+ assert len(error_steps) == 1
52
+
53
+ def test_nested_trace_scopes(self):
54
+ """Test that nested trace scopes create separate traces."""
55
+ outer_trace = None
56
+ inner_trace = None
57
+
58
+ with trace_scope("outer") as outer:
59
+ outer_trace = outer
60
+ with trace_scope("inner") as inner:
61
+ inner_trace = inner
62
+
63
+ assert outer_trace.name == "outer"
64
+ assert inner_trace.name == "inner"
65
+ assert outer_trace.id != inner_trace.id
66
+
67
+
68
+ class TestAddSteps:
69
+ """Test adding steps to trace."""
70
+
71
+ def test_add_thinking(self):
72
+ """Test adding thinking step."""
73
+ with trace_scope("test") as trace:
74
+ add_thinking("Test thinking")
75
+
76
+ thinking_steps = [s for s in trace.steps if s.type == StepType.THINKING]
77
+ assert len(thinking_steps) == 1
78
+ assert "Test thinking" in thinking_steps[0].content
79
+
80
+ def test_add_llm_call(self):
81
+ """Test adding LLM call step."""
82
+ with trace_scope("test") as trace:
83
+ add_llm_call(
84
+ prompt="Hello",
85
+ completion="World",
86
+ tokens_input=10,
87
+ tokens_output=5,
88
+ latency_ms=100.0,
89
+ )
90
+
91
+ llm_steps = [s for s in trace.steps if s.type == StepType.LLM_CALL]
92
+ assert len(llm_steps) == 1
93
+ assert llm_steps[0].tokens_input == 10
94
+ assert llm_steps[0].tokens_output == 5
95
+ assert llm_steps[0].latency_ms == 100.0
96
+
97
+ def test_add_tool_call(self):
98
+ """Test adding tool call step."""
99
+ with trace_scope("test") as trace:
100
+ add_tool_call(
101
+ tool_name="search",
102
+ arguments={"query": "python"},
103
+ result="Python is a programming language",
104
+ latency_ms=50.0,
105
+ )
106
+
107
+ tool_steps = [s for s in trace.steps if s.type == StepType.TOOL_CALL]
108
+ assert len(tool_steps) == 1
109
+ assert tool_steps[0].tool_call.name == "search"
110
+ assert tool_steps[0].tool_call.arguments == {"query": "python"}
111
+
112
+
113
+ class TestGetCurrentTrace:
114
+ """Test get_current_trace function."""
115
+
116
+ def test_get_current_trace_outside_scope(self):
117
+ """Test getting trace outside scope returns None."""
118
+ trace = get_current_trace()
119
+ assert trace is None
120
+
121
+ def test_get_current_trace_inside_scope(self):
122
+ """Test getting trace inside scope returns trace."""
123
+ with trace_scope("test") as expected_trace:
124
+ current = get_current_trace()
125
+ assert current is expected_trace
126
+
127
+
128
+ class TestInitMonitor:
129
+ """Test init_monitor function."""
130
+
131
+ @patch('agentscope.monitor.requests.post')
132
+ def test_init_monitor_sets_url(self, mock_post):
133
+ """Test that init_monitor sets the monitor URL."""
134
+ init_monitor("http://test-server:8000")
135
+ # Just verify no exception is raised
136
+ assert True
137
+
138
+
139
+ if __name__ == "__main__":
140
+ pytest.main([__file__, "-v"])