hivetrace 1.3.4__tar.gz → 1.3.5__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.
- {hivetrace-1.3.4 → hivetrace-1.3.5}/PKG-INFO +1 -1
- hivetrace-1.3.5/hivetrace/__init__.py +145 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/base_adapter.py +42 -6
- hivetrace-1.3.5/hivetrace/adapters/crewai/adapter.py +438 -0
- hivetrace-1.3.5/hivetrace/adapters/crewai/decorators.py +56 -0
- hivetrace-1.3.5/hivetrace/adapters/crewai/monitored_crew.py +155 -0
- hivetrace-1.3.5/hivetrace/adapters/crewai/tool_wrapper.py +69 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/langchain/__init__.py +3 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/langchain/adapter.py +67 -0
- hivetrace-1.3.5/hivetrace/adapters/langchain/api.py +264 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/openai_agents/tracing.py +8 -7
- hivetrace-1.3.5/hivetrace/client/__init__.py +15 -0
- hivetrace-1.3.5/hivetrace/client/async_client.py +114 -0
- hivetrace-1.3.5/hivetrace/client/base.py +180 -0
- hivetrace-1.3.5/hivetrace/client/sync_client.py +116 -0
- hivetrace-1.3.5/hivetrace/errors/__init__.py +55 -0
- hivetrace-1.3.5/hivetrace/errors/api.py +35 -0
- hivetrace-1.3.5/hivetrace/errors/base.py +30 -0
- hivetrace-1.3.5/hivetrace/errors/network.py +32 -0
- hivetrace-1.3.5/hivetrace/errors/validation.py +37 -0
- hivetrace-1.3.5/hivetrace/handlers/__init__.py +14 -0
- hivetrace-1.3.5/hivetrace/handlers/error_handler.py +119 -0
- hivetrace-1.3.5/hivetrace/handlers/response_builder.py +80 -0
- hivetrace-1.3.5/hivetrace/models/__init__.py +50 -0
- hivetrace-1.3.5/hivetrace/models/requests.py +88 -0
- hivetrace-1.3.5/hivetrace/models/responses.py +102 -0
- hivetrace-1.3.5/hivetrace/utils/__init__.py +37 -0
- hivetrace-1.3.5/hivetrace/utils/error_helpers.py +78 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/utils/uuid_generator.py +0 -9
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace.egg-info/PKG-INFO +1 -1
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace.egg-info/SOURCES.txt +17 -2
- {hivetrace-1.3.4 → hivetrace-1.3.5}/setup.py +1 -1
- hivetrace-1.3.4/hivetrace/__init__.py +0 -36
- hivetrace-1.3.4/hivetrace/adapters/crewai/adapter.py +0 -507
- hivetrace-1.3.4/hivetrace/adapters/crewai/decorators.py +0 -65
- hivetrace-1.3.4/hivetrace/adapters/crewai/monitored_crew.py +0 -182
- hivetrace-1.3.4/hivetrace/adapters/crewai/tool_wrapper.py +0 -95
- hivetrace-1.3.4/hivetrace/crewai_adapter.py +0 -9
- hivetrace-1.3.4/hivetrace/hivetrace.py +0 -270
- hivetrace-1.3.4/hivetrace/utils/__init__.py +0 -7
- {hivetrace-1.3.4 → hivetrace-1.3.5}/LICENSE +0 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/README.md +0 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/__init__.py +0 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/crewai/__init__.py +0 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/crewai/monitored_agent.py +0 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/langchain/behavior_tracker.py +0 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/langchain/callback.py +0 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/langchain/decorators.py +0 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/langchain/models.py +0 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/openai_agents/__init__.py +0 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/openai_agents/adapter.py +0 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/openai_agents/models.py +0 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/utils/__init__.py +0 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/utils/logging.py +0 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace.egg-info/dependency_links.txt +0 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace.egg-info/requires.txt +0 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace.egg-info/top_level.txt +0 -0
- {hivetrace-1.3.4 → hivetrace-1.3.5}/setup.cfg +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HiveTrace SDK - Python client for monitoring LLM applications.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Main clients
|
|
6
|
+
from .client import AsyncHivetraceSDK, BaseHivetraceSDK, SyncHivetraceSDK
|
|
7
|
+
|
|
8
|
+
# Exceptions (only exception classes)
|
|
9
|
+
from .errors import (
|
|
10
|
+
APIError,
|
|
11
|
+
ConfigurationError,
|
|
12
|
+
ConnectionError,
|
|
13
|
+
HiveTraceError,
|
|
14
|
+
HTTPError,
|
|
15
|
+
InvalidFormatError,
|
|
16
|
+
InvalidParameterError,
|
|
17
|
+
JSONDecodeError,
|
|
18
|
+
MissingConfigError,
|
|
19
|
+
MissingParameterError,
|
|
20
|
+
NetworkError,
|
|
21
|
+
RateLimitError,
|
|
22
|
+
RequestError,
|
|
23
|
+
TimeoutError,
|
|
24
|
+
UnauthorizedError,
|
|
25
|
+
ValidationError,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Handlers
|
|
29
|
+
from .handlers import ErrorHandler, ResponseBuilder
|
|
30
|
+
|
|
31
|
+
# Data models (Pydantic)
|
|
32
|
+
from .models import (
|
|
33
|
+
FunctionCallRequest,
|
|
34
|
+
HivetraceResponse,
|
|
35
|
+
InputRequest,
|
|
36
|
+
OutputRequest,
|
|
37
|
+
ProcessResponse,
|
|
38
|
+
SuccessResponse,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Utils
|
|
42
|
+
from .utils import (
|
|
43
|
+
generate_uuid,
|
|
44
|
+
get_error_details,
|
|
45
|
+
get_error_type,
|
|
46
|
+
get_status_code,
|
|
47
|
+
is_connection_error,
|
|
48
|
+
is_error_response,
|
|
49
|
+
is_http_error,
|
|
50
|
+
is_json_decode_error,
|
|
51
|
+
is_request_error,
|
|
52
|
+
is_success_response,
|
|
53
|
+
is_timeout_error,
|
|
54
|
+
is_validation_error,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
# Main classes
|
|
59
|
+
"AsyncHivetraceSDK",
|
|
60
|
+
"SyncHivetraceSDK",
|
|
61
|
+
"BaseHivetraceSDK",
|
|
62
|
+
# Exceptions
|
|
63
|
+
"HiveTraceError",
|
|
64
|
+
"ConfigurationError",
|
|
65
|
+
"MissingConfigError",
|
|
66
|
+
"UnauthorizedError",
|
|
67
|
+
"ValidationError",
|
|
68
|
+
"InvalidParameterError",
|
|
69
|
+
"MissingParameterError",
|
|
70
|
+
"InvalidFormatError",
|
|
71
|
+
"NetworkError",
|
|
72
|
+
"ConnectionError",
|
|
73
|
+
"TimeoutError",
|
|
74
|
+
"RequestError",
|
|
75
|
+
"APIError",
|
|
76
|
+
"HTTPError",
|
|
77
|
+
"JSONDecodeError",
|
|
78
|
+
"RateLimitError",
|
|
79
|
+
# Models
|
|
80
|
+
"HivetraceResponse",
|
|
81
|
+
"SuccessResponse",
|
|
82
|
+
"ProcessResponse",
|
|
83
|
+
"InputRequest",
|
|
84
|
+
"OutputRequest",
|
|
85
|
+
"FunctionCallRequest",
|
|
86
|
+
# Handlers
|
|
87
|
+
"ErrorHandler",
|
|
88
|
+
"ResponseBuilder",
|
|
89
|
+
# Utils
|
|
90
|
+
"generate_uuid",
|
|
91
|
+
"is_error_response",
|
|
92
|
+
"is_success_response",
|
|
93
|
+
"get_error_type",
|
|
94
|
+
"get_error_details",
|
|
95
|
+
"get_status_code",
|
|
96
|
+
"is_connection_error",
|
|
97
|
+
"is_timeout_error",
|
|
98
|
+
"is_http_error",
|
|
99
|
+
"is_json_decode_error",
|
|
100
|
+
"is_request_error",
|
|
101
|
+
"is_validation_error",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
# Optional adapters
|
|
105
|
+
try:
|
|
106
|
+
from hivetrace.adapters.crewai import CrewAIAdapter as _CrewAIAdapter
|
|
107
|
+
from hivetrace.adapters.crewai import trace as _crewai_trace
|
|
108
|
+
|
|
109
|
+
CrewAIAdapter = _CrewAIAdapter
|
|
110
|
+
crewai_trace = _crewai_trace
|
|
111
|
+
trace = _crewai_trace
|
|
112
|
+
|
|
113
|
+
__all__.extend(["CrewAIAdapter", "crewai_trace", "trace"])
|
|
114
|
+
except ImportError:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
from hivetrace.adapters.langchain import (
|
|
119
|
+
LangChainAdapter as _LangChainAdapter,
|
|
120
|
+
)
|
|
121
|
+
from hivetrace.adapters.langchain import (
|
|
122
|
+
run_with_tracing as _run_with_tracing,
|
|
123
|
+
)
|
|
124
|
+
from hivetrace.adapters.langchain import (
|
|
125
|
+
run_with_tracing_async as _run_with_tracing_async,
|
|
126
|
+
)
|
|
127
|
+
from hivetrace.adapters.langchain import (
|
|
128
|
+
trace as _langchain_trace,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
LangChainAdapter = _LangChainAdapter
|
|
132
|
+
langchain_trace = _langchain_trace
|
|
133
|
+
run_with_tracing = _run_with_tracing
|
|
134
|
+
run_with_tracing_async = _run_with_tracing_async
|
|
135
|
+
|
|
136
|
+
__all__.extend(
|
|
137
|
+
[
|
|
138
|
+
"LangChainAdapter",
|
|
139
|
+
"langchain_trace",
|
|
140
|
+
"run_with_tracing",
|
|
141
|
+
"run_with_tracing_async",
|
|
142
|
+
]
|
|
143
|
+
)
|
|
144
|
+
except ImportError:
|
|
145
|
+
pass
|
|
@@ -54,8 +54,14 @@ class BaseAdapter:
|
|
|
54
54
|
- additional_params_from_caller: Additional parameters to include in the log
|
|
55
55
|
"""
|
|
56
56
|
final_additional_params = additional_params_from_caller or {}
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
if hasattr(self, "user_id") and self.user_id is not None and self.user_id != "":
|
|
58
|
+
final_additional_params.setdefault("user_id", self.user_id)
|
|
59
|
+
if (
|
|
60
|
+
hasattr(self, "session_id")
|
|
61
|
+
and self.session_id is not None
|
|
62
|
+
and self.session_id != ""
|
|
63
|
+
):
|
|
64
|
+
final_additional_params.setdefault("session_id", self.session_id)
|
|
59
65
|
|
|
60
66
|
log_kwargs = {
|
|
61
67
|
"application_id": self.application_id,
|
|
@@ -71,19 +77,49 @@ class BaseAdapter:
|
|
|
71
77
|
if tool_call_details is None:
|
|
72
78
|
print("Warning: tool_call_details is None for function_call")
|
|
73
79
|
return
|
|
74
|
-
|
|
80
|
+
merged_tool_details = dict(tool_call_details)
|
|
81
|
+
ap = dict(merged_tool_details.get("additional_parameters", {}) or {})
|
|
82
|
+
if (
|
|
83
|
+
hasattr(self, "user_id")
|
|
84
|
+
and self.user_id is not None
|
|
85
|
+
and self.user_id != ""
|
|
86
|
+
):
|
|
87
|
+
ap.setdefault("user_id", self.user_id)
|
|
88
|
+
if (
|
|
89
|
+
hasattr(self, "session_id")
|
|
90
|
+
and self.session_id is not None
|
|
91
|
+
and self.session_id != ""
|
|
92
|
+
):
|
|
93
|
+
ap.setdefault("session_id", self.session_id)
|
|
94
|
+
if ap:
|
|
95
|
+
merged_tool_details["additional_parameters"] = ap
|
|
96
|
+
log_kwargs.update(merged_tool_details)
|
|
75
97
|
else:
|
|
76
98
|
print(f"Error: Unsupported log_method_name_stem: {log_method_name_stem}")
|
|
77
99
|
return
|
|
78
100
|
|
|
79
|
-
|
|
101
|
+
# Both SyncHivetraceSDK and AsyncHivetraceSDK expose the same method names
|
|
102
|
+
# (input/output/function_call). In async mode they are coroutines.
|
|
103
|
+
method_to_call_name = log_method_name_stem
|
|
80
104
|
|
|
81
105
|
try:
|
|
82
106
|
actual_log_method = getattr(self.trace, method_to_call_name)
|
|
83
107
|
if is_async:
|
|
84
108
|
import asyncio
|
|
85
|
-
|
|
86
|
-
|
|
109
|
+
import inspect
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
maybe_coro = actual_log_method(**log_kwargs)
|
|
113
|
+
except TypeError:
|
|
114
|
+
# Fallback: call without kwargs if signature mismatch (defensive)
|
|
115
|
+
maybe_coro = actual_log_method()
|
|
116
|
+
|
|
117
|
+
if inspect.isawaitable(maybe_coro):
|
|
118
|
+
asyncio.create_task(maybe_coro)
|
|
119
|
+
else:
|
|
120
|
+
# If the method is unexpectedly sync in async mode, call directly
|
|
121
|
+
# to avoid dropping the log.
|
|
122
|
+
pass
|
|
87
123
|
else:
|
|
88
124
|
actual_log_method(**log_kwargs)
|
|
89
125
|
except AttributeError:
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The main implementation of the CrewAI adapter.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
from crewai import Agent, Crew, Task
|
|
9
|
+
|
|
10
|
+
from hivetrace.adapters.base_adapter import BaseAdapter
|
|
11
|
+
from hivetrace.adapters.crewai.monitored_agent import MonitoredAgent
|
|
12
|
+
from hivetrace.adapters.crewai.monitored_crew import MonitoredCrew
|
|
13
|
+
from hivetrace.adapters.crewai.tool_wrapper import wrap_tool
|
|
14
|
+
from hivetrace.adapters.utils.logging import process_agent_params
|
|
15
|
+
from hivetrace.utils.uuid_generator import generate_uuid
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CrewAIAdapter(BaseAdapter):
|
|
19
|
+
"""Adapter for monitoring CrewAI agents with Hivetrace."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
hivetrace,
|
|
24
|
+
application_id: str,
|
|
25
|
+
user_id: Optional[str] = None,
|
|
26
|
+
session_id: Optional[str] = None,
|
|
27
|
+
agent_id_mapping: Optional[Dict[str, Dict[str, str]]] = None,
|
|
28
|
+
):
|
|
29
|
+
super().__init__(hivetrace, application_id, user_id, session_id)
|
|
30
|
+
self.agent_id_mapping = agent_id_mapping or {}
|
|
31
|
+
self.agents_info = {}
|
|
32
|
+
self._runtime_user_id = None
|
|
33
|
+
self._runtime_session_id = None
|
|
34
|
+
self._runtime_agents_conversation_id = None
|
|
35
|
+
self._current_parent_agent_id = None
|
|
36
|
+
self._last_active_agent_id = None
|
|
37
|
+
self._conversation_started = False
|
|
38
|
+
self._first_agent_logged = False
|
|
39
|
+
self._recent_messages = []
|
|
40
|
+
self._max_recent_messages = 5
|
|
41
|
+
|
|
42
|
+
def _reset_conversation_state(self):
|
|
43
|
+
"""Reset conversation state for new command execution."""
|
|
44
|
+
self._conversation_started = False
|
|
45
|
+
self._first_agent_logged = False
|
|
46
|
+
self._current_parent_agent_id = None
|
|
47
|
+
self._last_active_agent_id = None
|
|
48
|
+
|
|
49
|
+
def _set_current_parent(self, agent_id: str):
|
|
50
|
+
"""Set current agent as parent for subsequent operations."""
|
|
51
|
+
self._current_parent_agent_id = agent_id
|
|
52
|
+
self._last_active_agent_id = agent_id
|
|
53
|
+
|
|
54
|
+
def _clear_current_parent(self):
|
|
55
|
+
"""Clear current parent when agent finishes work."""
|
|
56
|
+
self._current_parent_agent_id = None
|
|
57
|
+
|
|
58
|
+
def _get_current_parent_id(self) -> Optional[str]:
|
|
59
|
+
"""Get ID of current parent agent."""
|
|
60
|
+
return self._current_parent_agent_id
|
|
61
|
+
|
|
62
|
+
def _get_last_active_agent_id(self) -> Optional[str]:
|
|
63
|
+
"""Get ID of last active agent."""
|
|
64
|
+
return self._last_active_agent_id
|
|
65
|
+
|
|
66
|
+
def _set_runtime_context(
|
|
67
|
+
self,
|
|
68
|
+
user_id: Optional[str] = None,
|
|
69
|
+
session_id: Optional[str] = None,
|
|
70
|
+
agent_conversation_id: Optional[str] = None,
|
|
71
|
+
):
|
|
72
|
+
"""Set execution context for runtime parameters."""
|
|
73
|
+
self._runtime_user_id = user_id
|
|
74
|
+
self._runtime_session_id = session_id
|
|
75
|
+
self._runtime_agents_conversation_id = agent_conversation_id
|
|
76
|
+
|
|
77
|
+
def _get_effective_user_id(self) -> Optional[str]:
|
|
78
|
+
return self._runtime_user_id or self.user_id
|
|
79
|
+
|
|
80
|
+
def _get_effective_session_id(self) -> Optional[str]:
|
|
81
|
+
return self._runtime_session_id or self.session_id
|
|
82
|
+
|
|
83
|
+
def _get_effective_agents_conversation_id(self) -> Optional[str]:
|
|
84
|
+
return self._runtime_agents_conversation_id
|
|
85
|
+
|
|
86
|
+
def _should_skip_deduplication(
|
|
87
|
+
self, message_content: Optional[str], force_log: bool
|
|
88
|
+
) -> bool:
|
|
89
|
+
"""Determine if deduplication should be skipped."""
|
|
90
|
+
if force_log or not message_content:
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
skip_patterns = ["[", "Thought", "working on"]
|
|
94
|
+
return any(pattern in message_content for pattern in skip_patterns)
|
|
95
|
+
|
|
96
|
+
def _handle_deduplication(self, message_content: str) -> bool:
|
|
97
|
+
"""Handle message deduplication. Returns True if message should be skipped."""
|
|
98
|
+
message_hash = hash(message_content)
|
|
99
|
+
if message_hash in self._recent_messages:
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
self._recent_messages.append(message_hash)
|
|
103
|
+
if len(self._recent_messages) > self._max_recent_messages:
|
|
104
|
+
self._recent_messages.pop(0)
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
def _prepare_effective_params(
|
|
108
|
+
self, additional_params_from_caller: Optional[Dict[str, Any]]
|
|
109
|
+
) -> Dict[str, Any]:
|
|
110
|
+
"""Prepare parameters with runtime values."""
|
|
111
|
+
params = (
|
|
112
|
+
additional_params_from_caller.copy()
|
|
113
|
+
if additional_params_from_caller
|
|
114
|
+
else {}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
for key, getter in [
|
|
118
|
+
("user_id", self._get_effective_user_id),
|
|
119
|
+
("session_id", self._get_effective_session_id),
|
|
120
|
+
("agent_conversation_id", self._get_effective_agents_conversation_id),
|
|
121
|
+
]:
|
|
122
|
+
value = getter()
|
|
123
|
+
if value:
|
|
124
|
+
params.setdefault(key, value)
|
|
125
|
+
|
|
126
|
+
params.setdefault("is_final_answer", False)
|
|
127
|
+
return params
|
|
128
|
+
|
|
129
|
+
def _handle_agent_parent_id(self, params: Dict[str, Any]) -> None:
|
|
130
|
+
"""Add parent_id to agents if needed."""
|
|
131
|
+
agents = params.get("agents")
|
|
132
|
+
if not isinstance(agents, dict):
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
if not self._first_agent_logged and not self._conversation_started:
|
|
136
|
+
self._first_agent_logged = True
|
|
137
|
+
self._conversation_started = True
|
|
138
|
+
elif self._current_parent_agent_id:
|
|
139
|
+
for agent_info in agents.values():
|
|
140
|
+
if isinstance(agent_info, dict):
|
|
141
|
+
agent_info["agent_parent_id"] = self._current_parent_agent_id
|
|
142
|
+
|
|
143
|
+
def _build_log_kwargs(
|
|
144
|
+
self,
|
|
145
|
+
log_type: str,
|
|
146
|
+
message_content: Optional[str],
|
|
147
|
+
tool_call_details: Optional[Dict[str, Any]],
|
|
148
|
+
params: Dict[str, Any],
|
|
149
|
+
) -> Optional[Dict[str, Any]]:
|
|
150
|
+
"""Build arguments for logging method."""
|
|
151
|
+
base_kwargs = {
|
|
152
|
+
"application_id": self.application_id,
|
|
153
|
+
"additional_parameters": params,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if log_type in ["input", "output"]:
|
|
157
|
+
if message_content is None:
|
|
158
|
+
return None
|
|
159
|
+
base_kwargs["message"] = message_content
|
|
160
|
+
elif log_type == "function_call":
|
|
161
|
+
if tool_call_details is None:
|
|
162
|
+
return None
|
|
163
|
+
base_kwargs.update(tool_call_details)
|
|
164
|
+
else:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
return base_kwargs
|
|
168
|
+
|
|
169
|
+
def _execute_log(
|
|
170
|
+
self, log_type: str, is_async: bool, kwargs: Dict[str, Any]
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Execute logging with error handling."""
|
|
173
|
+
method_name = f"{log_type}{'_async' if is_async else ''}"
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
method = getattr(self.trace, method_name)
|
|
177
|
+
if is_async:
|
|
178
|
+
asyncio.create_task(method(**kwargs))
|
|
179
|
+
else:
|
|
180
|
+
method(**kwargs)
|
|
181
|
+
except AttributeError:
|
|
182
|
+
print(f"Error: Hivetrace object does not have method {method_name}")
|
|
183
|
+
except Exception as e:
|
|
184
|
+
print(f"Error logging {log_type} to Hivetrace: {e}")
|
|
185
|
+
|
|
186
|
+
def _prepare_and_log(
|
|
187
|
+
self,
|
|
188
|
+
log_type: str,
|
|
189
|
+
is_async: bool,
|
|
190
|
+
message_content: Optional[str] = None,
|
|
191
|
+
tool_call_details: Optional[Dict[str, Any]] = None,
|
|
192
|
+
additional_params_from_caller: Optional[Dict[str, Any]] = None,
|
|
193
|
+
force_log: bool = False,
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Central logging method with deduplication and parameter handling."""
|
|
196
|
+
if (
|
|
197
|
+
not self._should_skip_deduplication(message_content, force_log)
|
|
198
|
+
and message_content
|
|
199
|
+
):
|
|
200
|
+
if self._handle_deduplication(message_content):
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
params = self._prepare_effective_params(additional_params_from_caller)
|
|
204
|
+
self._handle_agent_parent_id(params)
|
|
205
|
+
|
|
206
|
+
kwargs = self._build_log_kwargs(
|
|
207
|
+
log_type, message_content, tool_call_details, params
|
|
208
|
+
)
|
|
209
|
+
if kwargs:
|
|
210
|
+
self._execute_log(log_type, is_async, kwargs)
|
|
211
|
+
|
|
212
|
+
def _get_agent_mapping(self, role: str) -> Dict[str, str]:
|
|
213
|
+
"""Get agent ID and description from mapping."""
|
|
214
|
+
mapping = self.agent_id_mapping.get(role, {})
|
|
215
|
+
|
|
216
|
+
if isinstance(mapping, dict):
|
|
217
|
+
return {
|
|
218
|
+
"id": mapping.get("id", generate_uuid()),
|
|
219
|
+
"description": mapping.get("description", ""),
|
|
220
|
+
}
|
|
221
|
+
elif isinstance(mapping, str):
|
|
222
|
+
return {"id": mapping, "description": ""}
|
|
223
|
+
|
|
224
|
+
return {"id": generate_uuid(), "description": ""}
|
|
225
|
+
|
|
226
|
+
def _log_output(
|
|
227
|
+
self, message: str, additional_params: Optional[Dict[str, Any]], is_async: bool
|
|
228
|
+
):
|
|
229
|
+
"""Common output logging logic."""
|
|
230
|
+
processed_params = process_agent_params(additional_params)
|
|
231
|
+
self._prepare_and_log(
|
|
232
|
+
"output",
|
|
233
|
+
is_async,
|
|
234
|
+
message_content=message,
|
|
235
|
+
additional_params_from_caller=processed_params,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
async def output_async(
|
|
239
|
+
self, message: str, additional_params: Optional[Dict[str, Any]] = None
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Asynchronous logging of agent output."""
|
|
242
|
+
if not self.async_mode:
|
|
243
|
+
raise RuntimeError("Cannot use async methods when SDK is in sync mode")
|
|
244
|
+
self._log_output(message, additional_params, True)
|
|
245
|
+
|
|
246
|
+
def output(
|
|
247
|
+
self, message: str, additional_params: Optional[Dict[str, Any]] = None
|
|
248
|
+
) -> None:
|
|
249
|
+
"""Synchronous logging of agent output."""
|
|
250
|
+
if self.async_mode:
|
|
251
|
+
raise RuntimeError("Cannot use sync methods when SDK is in async mode")
|
|
252
|
+
self._log_output(message, additional_params, False)
|
|
253
|
+
|
|
254
|
+
def agent_callback(self, message: Any) -> None:
|
|
255
|
+
"""Callback for agent actions."""
|
|
256
|
+
if isinstance(message, dict) and message.get("type") == "agent_thought":
|
|
257
|
+
self._handle_agent_thought_message(message)
|
|
258
|
+
else:
|
|
259
|
+
self._handle_generic_agent_message(message)
|
|
260
|
+
|
|
261
|
+
def _handle_agent_thought_message(self, message: Dict[str, Any]) -> None:
|
|
262
|
+
"""Handle agent thought type messages."""
|
|
263
|
+
role = message.get("role", "")
|
|
264
|
+
agent_mapping = self._get_agent_mapping(role)
|
|
265
|
+
final_agent_id = message.get("agent_id") or agent_mapping["id"]
|
|
266
|
+
|
|
267
|
+
agent_info = {
|
|
268
|
+
final_agent_id: {
|
|
269
|
+
"name": message.get("agent_name", role),
|
|
270
|
+
"description": agent_mapping.get("description")
|
|
271
|
+
or message.get("agent_description", "Agent thought"),
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
message_text = f"Thought from agent {role}: {message['thought']}"
|
|
276
|
+
self._prepare_and_log(
|
|
277
|
+
"input",
|
|
278
|
+
self.async_mode,
|
|
279
|
+
message_content=message_text,
|
|
280
|
+
additional_params_from_caller={"agents": agent_info},
|
|
281
|
+
force_log=True,
|
|
282
|
+
)
|
|
283
|
+
self._set_current_parent(final_agent_id)
|
|
284
|
+
|
|
285
|
+
def _handle_generic_agent_message(self, message: Any) -> None:
|
|
286
|
+
"""Handle generic agent messages."""
|
|
287
|
+
message_text = str(message)
|
|
288
|
+
self._prepare_and_log(
|
|
289
|
+
"input",
|
|
290
|
+
self.async_mode,
|
|
291
|
+
message_content=message_text,
|
|
292
|
+
additional_params_from_caller={"agents": self.agents_info},
|
|
293
|
+
force_log=True,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def task_callback(self, message: Any) -> None:
|
|
297
|
+
"""Handler for task messages."""
|
|
298
|
+
if not hasattr(message, "__dict__"):
|
|
299
|
+
self._handle_simple_task_message(message)
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
agent_info, current_agent_id = self._extract_agent_info_from_task(message)
|
|
303
|
+
message_content = self._extract_message_content_from_task(message)
|
|
304
|
+
|
|
305
|
+
if message_content:
|
|
306
|
+
self._prepare_and_log(
|
|
307
|
+
"output",
|
|
308
|
+
self.async_mode,
|
|
309
|
+
message_content=message_content,
|
|
310
|
+
additional_params_from_caller={"agents": agent_info},
|
|
311
|
+
force_log=True,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
if current_agent_id:
|
|
315
|
+
self._set_current_parent(current_agent_id)
|
|
316
|
+
|
|
317
|
+
def _extract_agent_info_from_task(
|
|
318
|
+
self, message: Any
|
|
319
|
+
) -> tuple[Dict[str, Any], Optional[str]]:
|
|
320
|
+
"""Extract agent information from task message."""
|
|
321
|
+
agent_info = {}
|
|
322
|
+
current_agent_id = None
|
|
323
|
+
current_agent_role = ""
|
|
324
|
+
|
|
325
|
+
if hasattr(message, "agent"):
|
|
326
|
+
agent_value = message.agent
|
|
327
|
+
if isinstance(agent_value, str):
|
|
328
|
+
current_agent_role = agent_value
|
|
329
|
+
elif hasattr(agent_value, "role"):
|
|
330
|
+
current_agent_role = agent_value.role
|
|
331
|
+
|
|
332
|
+
if current_agent_role:
|
|
333
|
+
agent_mapping = self._get_agent_mapping(current_agent_role)
|
|
334
|
+
current_agent_id = agent_mapping["id"]
|
|
335
|
+
|
|
336
|
+
description = agent_mapping["description"] or (
|
|
337
|
+
getattr(agent_value, "goal", "")
|
|
338
|
+
if hasattr(agent_value, "goal")
|
|
339
|
+
else "Task agent"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
agent_info = {
|
|
343
|
+
current_agent_id: {
|
|
344
|
+
"name": current_agent_role,
|
|
345
|
+
"description": description,
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return agent_info, current_agent_id
|
|
350
|
+
|
|
351
|
+
def _extract_message_content_from_task(self, message: Any) -> str:
|
|
352
|
+
"""Extract message content from task message."""
|
|
353
|
+
if hasattr(message, "raw") and message.raw:
|
|
354
|
+
return str(message.raw)
|
|
355
|
+
|
|
356
|
+
message_parts = []
|
|
357
|
+
for field in ["status", "step", "action", "observation", "thought"]:
|
|
358
|
+
if hasattr(message, field):
|
|
359
|
+
value = getattr(message, field)
|
|
360
|
+
if value:
|
|
361
|
+
message_parts.append(f"{field}: {str(value)}")
|
|
362
|
+
|
|
363
|
+
return "; ".join(message_parts) if message_parts else str(message)
|
|
364
|
+
|
|
365
|
+
def _handle_simple_task_message(self, message: Any) -> None:
|
|
366
|
+
"""Handle simple task messages without attributes."""
|
|
367
|
+
message_text = f"[Task] {str(message)}"
|
|
368
|
+
self._prepare_and_log(
|
|
369
|
+
"output",
|
|
370
|
+
self.async_mode,
|
|
371
|
+
message_content=message_text,
|
|
372
|
+
additional_params_from_caller={"agents": {}},
|
|
373
|
+
force_log=True,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def _wrap_agent(self, agent: Agent) -> Agent:
|
|
377
|
+
"""Wrap agent for monitoring."""
|
|
378
|
+
agent_mapping = self._get_agent_mapping(agent.role)
|
|
379
|
+
agent_id = agent_mapping["id"]
|
|
380
|
+
|
|
381
|
+
agent_props = agent.__dict__.copy()
|
|
382
|
+
original_tools = getattr(agent, "tools", [])
|
|
383
|
+
agent_props["tools"] = [
|
|
384
|
+
wrap_tool(tool, agent.role, self) for tool in original_tools
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
for key in ["id", "agent_executor", "agent_ops_agent_id"]:
|
|
388
|
+
agent_props.pop(key, None)
|
|
389
|
+
|
|
390
|
+
return MonitoredAgent(
|
|
391
|
+
adapter_instance=self,
|
|
392
|
+
callback_func=self.agent_callback,
|
|
393
|
+
agent_id=agent_id,
|
|
394
|
+
**agent_props,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
def _wrap_task(self, task: Task) -> Task:
|
|
398
|
+
"""Wrap task for monitoring."""
|
|
399
|
+
original_callback = task.callback
|
|
400
|
+
|
|
401
|
+
def combined_callback(message):
|
|
402
|
+
self.task_callback(message)
|
|
403
|
+
if original_callback:
|
|
404
|
+
original_callback(message)
|
|
405
|
+
|
|
406
|
+
task.callback = combined_callback
|
|
407
|
+
return task
|
|
408
|
+
|
|
409
|
+
def wrap_crew(self, crew: Crew) -> Crew:
|
|
410
|
+
"""Add monitoring to CrewAI crew."""
|
|
411
|
+
self._reset_conversation_state()
|
|
412
|
+
|
|
413
|
+
agents_info = {}
|
|
414
|
+
for agent in crew.agents:
|
|
415
|
+
if hasattr(agent, "role"):
|
|
416
|
+
agent_mapping = self._get_agent_mapping(agent.role)
|
|
417
|
+
agent_id = agent_mapping["id"]
|
|
418
|
+
description = agent_mapping["description"] or getattr(agent, "goal", "")
|
|
419
|
+
agents_info[agent_id] = {
|
|
420
|
+
"name": agent.role,
|
|
421
|
+
"description": description,
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
self.agents_info = agents_info
|
|
425
|
+
|
|
426
|
+
wrapped_agents = [self._wrap_agent(agent) for agent in crew.agents]
|
|
427
|
+
wrapped_tasks = [self._wrap_task(task) for task in crew.tasks]
|
|
428
|
+
|
|
429
|
+
return MonitoredCrew(
|
|
430
|
+
original_crew_agents=wrapped_agents,
|
|
431
|
+
original_crew_tasks=wrapped_tasks,
|
|
432
|
+
original_crew_verbose=crew.verbose,
|
|
433
|
+
manager_llm=getattr(crew, "manager_llm", None),
|
|
434
|
+
memory=getattr(crew, "memory", None),
|
|
435
|
+
process=getattr(crew, "process", None),
|
|
436
|
+
config=getattr(crew, "config", None),
|
|
437
|
+
adapter=self,
|
|
438
|
+
)
|