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.
Files changed (58) hide show
  1. {hivetrace-1.3.4 → hivetrace-1.3.5}/PKG-INFO +1 -1
  2. hivetrace-1.3.5/hivetrace/__init__.py +145 -0
  3. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/base_adapter.py +42 -6
  4. hivetrace-1.3.5/hivetrace/adapters/crewai/adapter.py +438 -0
  5. hivetrace-1.3.5/hivetrace/adapters/crewai/decorators.py +56 -0
  6. hivetrace-1.3.5/hivetrace/adapters/crewai/monitored_crew.py +155 -0
  7. hivetrace-1.3.5/hivetrace/adapters/crewai/tool_wrapper.py +69 -0
  8. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/langchain/__init__.py +3 -0
  9. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/langchain/adapter.py +67 -0
  10. hivetrace-1.3.5/hivetrace/adapters/langchain/api.py +264 -0
  11. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/openai_agents/tracing.py +8 -7
  12. hivetrace-1.3.5/hivetrace/client/__init__.py +15 -0
  13. hivetrace-1.3.5/hivetrace/client/async_client.py +114 -0
  14. hivetrace-1.3.5/hivetrace/client/base.py +180 -0
  15. hivetrace-1.3.5/hivetrace/client/sync_client.py +116 -0
  16. hivetrace-1.3.5/hivetrace/errors/__init__.py +55 -0
  17. hivetrace-1.3.5/hivetrace/errors/api.py +35 -0
  18. hivetrace-1.3.5/hivetrace/errors/base.py +30 -0
  19. hivetrace-1.3.5/hivetrace/errors/network.py +32 -0
  20. hivetrace-1.3.5/hivetrace/errors/validation.py +37 -0
  21. hivetrace-1.3.5/hivetrace/handlers/__init__.py +14 -0
  22. hivetrace-1.3.5/hivetrace/handlers/error_handler.py +119 -0
  23. hivetrace-1.3.5/hivetrace/handlers/response_builder.py +80 -0
  24. hivetrace-1.3.5/hivetrace/models/__init__.py +50 -0
  25. hivetrace-1.3.5/hivetrace/models/requests.py +88 -0
  26. hivetrace-1.3.5/hivetrace/models/responses.py +102 -0
  27. hivetrace-1.3.5/hivetrace/utils/__init__.py +37 -0
  28. hivetrace-1.3.5/hivetrace/utils/error_helpers.py +78 -0
  29. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/utils/uuid_generator.py +0 -9
  30. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace.egg-info/PKG-INFO +1 -1
  31. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace.egg-info/SOURCES.txt +17 -2
  32. {hivetrace-1.3.4 → hivetrace-1.3.5}/setup.py +1 -1
  33. hivetrace-1.3.4/hivetrace/__init__.py +0 -36
  34. hivetrace-1.3.4/hivetrace/adapters/crewai/adapter.py +0 -507
  35. hivetrace-1.3.4/hivetrace/adapters/crewai/decorators.py +0 -65
  36. hivetrace-1.3.4/hivetrace/adapters/crewai/monitored_crew.py +0 -182
  37. hivetrace-1.3.4/hivetrace/adapters/crewai/tool_wrapper.py +0 -95
  38. hivetrace-1.3.4/hivetrace/crewai_adapter.py +0 -9
  39. hivetrace-1.3.4/hivetrace/hivetrace.py +0 -270
  40. hivetrace-1.3.4/hivetrace/utils/__init__.py +0 -7
  41. {hivetrace-1.3.4 → hivetrace-1.3.5}/LICENSE +0 -0
  42. {hivetrace-1.3.4 → hivetrace-1.3.5}/README.md +0 -0
  43. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/__init__.py +0 -0
  44. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/crewai/__init__.py +0 -0
  45. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/crewai/monitored_agent.py +0 -0
  46. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/langchain/behavior_tracker.py +0 -0
  47. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/langchain/callback.py +0 -0
  48. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/langchain/decorators.py +0 -0
  49. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/langchain/models.py +0 -0
  50. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/openai_agents/__init__.py +0 -0
  51. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/openai_agents/adapter.py +0 -0
  52. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/openai_agents/models.py +0 -0
  53. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/utils/__init__.py +0 -0
  54. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace/adapters/utils/logging.py +0 -0
  55. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace.egg-info/dependency_links.txt +0 -0
  56. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace.egg-info/requires.txt +0 -0
  57. {hivetrace-1.3.4 → hivetrace-1.3.5}/hivetrace.egg-info/top_level.txt +0 -0
  58. {hivetrace-1.3.4 → hivetrace-1.3.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hivetrace
3
- Version: 1.3.4
3
+ Version: 1.3.5
4
4
  Summary: Hivetrace SDK for monitoring LLM applications
5
5
  Home-page: http://hivetrace.ai
6
6
  Author: Raft
@@ -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
- final_additional_params.setdefault("user_id", self.user_id)
58
- final_additional_params.setdefault("session_id", self.session_id)
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
- log_kwargs.update(tool_call_details)
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
- method_to_call_name = f"{log_method_name_stem}{'_async' if is_async else ''}"
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
- asyncio.create_task(actual_log_method(**log_kwargs))
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
+ )