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.
- ai_agent_scope-0.2.0/PKG-INFO +80 -0
- ai_agent_scope-0.2.0/README.md +34 -0
- ai_agent_scope-0.2.0/agentscope/__init__.py +43 -0
- ai_agent_scope-0.2.0/agentscope/models.py +126 -0
- ai_agent_scope-0.2.0/agentscope/monitor.py +537 -0
- ai_agent_scope-0.2.0/ai_agent_scope.egg-info/PKG-INFO +80 -0
- ai_agent_scope-0.2.0/ai_agent_scope.egg-info/SOURCES.txt +13 -0
- ai_agent_scope-0.2.0/ai_agent_scope.egg-info/dependency_links.txt +1 -0
- ai_agent_scope-0.2.0/ai_agent_scope.egg-info/requires.txt +9 -0
- ai_agent_scope-0.2.0/ai_agent_scope.egg-info/top_level.txt +2 -0
- ai_agent_scope-0.2.0/setup.cfg +4 -0
- ai_agent_scope-0.2.0/setup.py +50 -0
- ai_agent_scope-0.2.0/tests/__init__.py +1 -0
- ai_agent_scope-0.2.0/tests/test_models.py +158 -0
- ai_agent_scope-0.2.0/tests/test_monitor.py +140 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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"])
|