diff --git a/pyproject.toml b/pyproject.toml index c9e1aa6d..c144c4bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,10 +27,10 @@ packages = [ [project.optional-dependencies] langchain = ["langchain-core", "langchain", "langsmith (>=0.8.0)"] -openai = ["openai (>=2.8.0,<3.0.0)", "packaging (>=24.2,<25.0)", "openai-agents (>=0.4.0,<1.0.0)", "cryptography (>=50.0.0)", "mcp (>=1.27.2,<2)"] -crewai = ["crewai (>=0.152.0,<2.0.0); python_version < '3.14'", "litellm (>=1.83.14,<2.0.0); python_version < '3.14'", "uv (>=0.9.6); python_version < '3.14'", "aiohttp (>=3.14.1,<4); python_version < '3.14'", "cryptography (>=50.0.0)", "mcp (>=1.27.2,<2)", "pdfminer-six (>=20251107)"] +openai = ["openai (>=3.0.0,<4.0.0)", "packaging (>=24.2,<25.0)", "openai-agents (>=0.21.0,<1.0.0)", "cryptography (>=50.0.0)", "mcp (>=1.27.2,<2)"] +crewai = ["crewai (>=0.177.0,<2.0.0); python_version < '3.14'", "uv (>=0.9.6); python_version < '3.14'", "aiohttp (>=3.14.1,<4); python_version < '3.14'", "cryptography (>=50.0.0)", "mcp (>=1.27.2,<2)", "pdfminer-six (>=20251107)"] middleware = ["starlette"] -all = ["langchain-core", "langchain", "langsmith (>=0.8.0)", "openai (>=2.8.0,<3.0.0)", "packaging (>=24.2,<25.0)", "openai-agents (>=0.4.0,<1.0.0)", "crewai (>=0.152.0,<2.0.0); python_version < '3.14'", "starlette", "litellm (>=1.83.14,<2.0.0); python_version < '3.14'", "uv (>=0.9.6); python_version < '3.14'", "aiohttp (>=3.14.1,<4); python_version < '3.14'", "cryptography (>=50.0.0)", "mcp (>=1.27.2,<2)", "pdfminer-six (>=20251107)"] +all = ["langchain-core", "langchain", "langsmith (>=0.8.0)", "openai (>=3.0.0,<4.0.0)", "packaging (>=24.2,<25.0)", "openai-agents (>=0.21.0,<1.0.0)", "crewai (>=0.177.0,<2.0.0); python_version < '3.14'", "starlette", "uv (>=0.9.6); python_version < '3.14'", "aiohttp (>=3.14.1,<4); python_version < '3.14'", "cryptography (>=50.0.0)", "mcp (>=1.27.2,<2)", "pdfminer-six (>=20251107)"] @@ -45,14 +45,12 @@ python-dateutil = "^2.8.0" langchain-core = { version = ">=1.3.3", optional = true } langchain = { version = ">=1.3.9,<2.0.0", optional = true } langsmith = { version = ">=0.8.0", optional = true } -openai = { version = ">=2.8.0,<3.0.0", optional = true } -openai-agents = { version = ">=0.4.0,<1.0.0", optional = true } -litellm = { version = ">=1.83.14,<2.0.0", optional = true, python = ">=3.11,<3.14" } +openai = { version = ">=3.0.0,<4.0.0", optional = true } +openai-agents = { version = ">=0.21.0,<1.0.0", optional = true } galileo-core = "^4.4.0" httpx = ">=0.27.0,<0.29.0" starlette = { version = ">=1.3.1", optional = true } backoff = "^2.2.1" -crewai = { version = ">=0.152.0,<2.0.0", optional = true, python = ">=3.11,<3.14" } tqdm = { version = ">=4.0.0" } typing-extensions = { version = ">=4.5.0" } opentelemetry-sdk = "^1.38.0" @@ -93,7 +91,7 @@ time-machine = "^2.17.0" # freezegun causes problems with pydantic model valida pre-commit = "^4.0.1" mypy = "^1.16.0" invoke = "^2.2.0" -openai = ">=2.8.0,<3.0.0" +openai = ">=3.0.0,<4.0.0" fastapi = "^0.135.0" ruff = "^0.15.22" openapi-python-client = "^0.29.0" diff --git a/src/splunk_ao/handlers/crewai/handler.py b/src/splunk_ao/handlers/crewai/handler.py index 6adc870c..5d346ab9 100644 --- a/src/splunk_ao/handlers/crewai/handler.py +++ b/src/splunk_ao/handlers/crewai/handler.py @@ -1,7 +1,6 @@ import hashlib import json import logging -from datetime import datetime from typing import Any from uuid import UUID @@ -15,19 +14,16 @@ _logger = logging.getLogger(__name__) # Lazy import state — crewai wraps sys.stdout/sys.stderr at import time, which -# causes pytest-xdist worker hangs. All crewai/litellm imports are deferred to +# causes pytest-xdist worker hangs. All crewai imports are deferred to # _resolve_crewai_imports(), called once from CrewAIEventListener.__init__. CREWAI_AVAILABLE = False CREWAI_EVENTS_MODULE_AVAILABLE = False -LITE_LLM_AVAILABLE = False -litellm = None _crewai_imports_resolved = False def _resolve_crewai_imports() -> None: - """Import crewai and litellm on first use, populating module-level globals.""" - global CREWAI_AVAILABLE, CREWAI_EVENTS_MODULE_AVAILABLE - global LITE_LLM_AVAILABLE, litellm, _crewai_imports_resolved + """Import crewai on first use, populating module-level globals.""" + global CREWAI_AVAILABLE, CREWAI_EVENTS_MODULE_AVAILABLE, _crewai_imports_resolved if _crewai_imports_resolved: return @@ -43,15 +39,6 @@ def _resolve_crewai_imports() -> None: except ImportError: _logger.warning("CrewAI not available, using stubs") - try: - import litellm as _litellm - - litellm = _litellm - LITE_LLM_AVAILABLE = True - except ImportError: - _logger.warning("LiteLLM not available, using stubs") - litellm = None - class CrewAIEventListener: """ @@ -92,11 +79,6 @@ def __init__( except ImportError: _logger.warning("Could not import crewai event bus, skipping listener setup") - if LITE_LLM_AVAILABLE and litellm is not None: - if not litellm.success_callback: - litellm.success_callback = [] - litellm.success_callback.append(self.lite_llm_usage_callback) - def setup_listeners(self, crewai_event_bus: Any) -> None: """Setup event listeners for CrewAI events.""" if not CREWAI_AVAILABLE: @@ -345,12 +327,7 @@ def _generate_run_id(self, source: Any, event: Any) -> UUID: task_id = getattr(getattr(event, "task", None), "id", "") return self._hash_to_uuid(f"{event_agent.id}_{task_id}") - # 7. dict messages — lite_llm callback - if isinstance(event, dict) and "messages" in event: - messages = json.dumps(event["messages"]) - return self._hash_to_uuid(messages) - - # 8. Generic fallback + # 7. Generic fallback return self._hash_to_uuid( f"{getattr(event, 'crew_name', '')}_{getattr(event, 'agent', '')}_{getattr(event, 'task', '')}" ) @@ -770,21 +747,3 @@ def _handle_memory_retrieval_failed(self, source: Any, event: Any) -> None: self._handler.end_node(run_id=run_id, output=f"Memory retrieval failed: {metadata['error']}", metadata=metadata) - def lite_llm_usage_callback( - self, - kwargs: dict, # kwargs to completion - completion_response: Any, # response from completion - start_time: datetime, - end_time: datetime, - ) -> None: - node_id = self._generate_run_id(kwargs, kwargs) - - node = self._handler.get_node(node_id) - if not node: - _logger.debug(f"No node exists for run_id {node_id}") - return - usage = completion_response.model_extra["usage"] - node.span_params["usage"] = usage.model_dump() if hasattr(usage, "model_dump") else usage - node.span_params["num_input_tokens"] = getattr(usage, "prompt_tokens", 0) - node.span_params["num_output_tokens"] = getattr(usage, "completion_tokens", 0) - node.span_params["total_tokens"] = getattr(usage, "total_tokens", 0) diff --git a/tests/test_crewai_handler.py b/tests/test_crewai_handler.py index 61cd33e5..ebe58e20 100644 --- a/tests/test_crewai_handler.py +++ b/tests/test_crewai_handler.py @@ -99,7 +99,6 @@ def crewai_callback(mock_splunk_ao_logger): with ( patch("splunk_ao.handlers.crewai.handler._crewai_imports_resolved", True), patch("splunk_ao.handlers.crewai.handler.CREWAI_AVAILABLE", False), - patch("splunk_ao.handlers.crewai.handler.LITE_LLM_AVAILABLE", False), ): from splunk_ao.handlers.crewai.handler import CrewAIEventListener @@ -113,7 +112,6 @@ def test_initialization_with_crewai_available(mock_splunk_ao_logger) -> None: with ( patch("splunk_ao.handlers.crewai.handler._crewai_imports_resolved", True), patch("splunk_ao.handlers.crewai.handler.CREWAI_AVAILABLE", True), - patch("splunk_ao.handlers.crewai.handler.LITE_LLM_AVAILABLE", True), ): from splunk_ao.handlers.crewai.handler import CrewAIEventListener @@ -131,7 +129,6 @@ def test_initialization_with_crewai_unavailable(mock_splunk_ao_logger) -> None: with ( patch("splunk_ao.handlers.crewai.handler._crewai_imports_resolved", True), patch("splunk_ao.handlers.crewai.handler.CREWAI_AVAILABLE", False), - patch("splunk_ao.handlers.crewai.handler.LITE_LLM_AVAILABLE", False), ): from splunk_ao.handlers.crewai.handler import CrewAIEventListener @@ -693,55 +690,6 @@ def test_update_crew_input(crewai_callback: CrewAIEventListener) -> None: assert "Research market trends" in mock_root_node.span_params["input"] -def test_lite_llm_usage_callback(crewai_callback: CrewAIEventListener) -> None: - """Test LiteLLM usage callback.""" - node_id = uuid.uuid4() - - # Mock node - mock_node = Mock() - mock_node.span_params = {} - - # Mock completion response with usage - mock_usage = Mock() - mock_usage.model_dump.return_value = {"prompt_tokens": 100, "completion_tokens": 50, "total_tokens": 150} - mock_usage.prompt_tokens = 100 - mock_usage.completion_tokens = 50 - mock_usage.total_tokens = 150 - - mock_response = Mock() - mock_response.model_extra = {"usage": mock_usage} - - kwargs = {"messages": [{"role": "user", "content": "test"}]} - - with ( - patch.object(crewai_callback._handler, "get_node", return_value=mock_node), - patch.object(crewai_callback, "_generate_run_id", return_value=node_id), - ): - crewai_callback.lite_llm_usage_callback( - kwargs=kwargs, completion_response=mock_response, start_time=datetime.now(), end_time=datetime.now() - ) - - # Verify usage was recorded - assert mock_node.span_params["num_input_tokens"] == 100 - assert mock_node.span_params["num_output_tokens"] == 50 - assert mock_node.span_params["total_tokens"] == 150 - - -def test_lite_llm_usage_callback_no_node(crewai_callback) -> None: - """Test LiteLLM usage callback when node doesn't exist.""" - kwargs = {"messages": [{"role": "user", "content": "test"}]} - mock_response = Mock() - - with ( - patch.object(crewai_callback._handler, "get_node", return_value=None), - patch.object(crewai_callback, "_generate_run_id", return_value=uuid.uuid4()), - ): - # Should not raise an exception - crewai_callback.lite_llm_usage_callback( - kwargs=kwargs, completion_response=mock_response, start_time=datetime.now(), end_time=datetime.now() - ) - - # Memory event tests (for CrewAI >= 0.177.0) diff --git a/tests/test_openai_agents.py b/tests/test_openai_agents.py index 16ed813e..2a251ee0 100644 --- a/tests/test_openai_agents.py +++ b/tests/test_openai_agents.py @@ -2,9 +2,8 @@ import uuid from unittest.mock import MagicMock, Mock, patch -import httpx +import httpx2 import pytest -import respx import vcr from agents import ( Agent, @@ -12,10 +11,13 @@ GuardrailFunctionOutput, InputGuardrail, InputGuardrailTripwireTriggered, + OpenAIProvider, + RunConfig, Runner, set_trace_processors, ) from agents.tracing import ResponseSpanData +from openai import AsyncOpenAI from pydantic import BaseModel from pytest import MonkeyPatch, mark @@ -333,16 +335,22 @@ async def capture_payload(payload): mock_response_data = _create_mock_response_with_tools(tool_calls) - with respx.mock(base_url="https://api.openai.com") as respx_mock: - respx_mock.post("/v1/responses").mock(return_value=httpx.Response(200, json=mock_response_data)) - agent = Agent( - name="Assistant", - instructions="You are a helpful assistant.", - tools=[CodeInterpreterTool(tool_config={"type": "code_interpreter", "container": {"type": "auto"}})], - ) + def mock_handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json=mock_response_data) - result = await Runner.run(agent, "Test multiple tools.") - assert result + mock_openai = AsyncOpenAI( + api_key="sk-test", + http_client=httpx2.AsyncClient(transport=httpx2.MockTransport(mock_handler)), + ) + provider = OpenAIProvider(openai_client=mock_openai) + agent = Agent( + name="Assistant", + instructions="You are a helpful assistant.", + tools=[CodeInterpreterTool(tool_config={"type": "code_interpreter", "container": {"type": "auto"}})], + ) + + result = await Runner.run(agent, "Test multiple tools.", run_config=RunConfig(model_provider=provider)) + assert result traces = splunk_ao_logger.traces assert len(traces) == 1