Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions python/crewai/sample_agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
create_agent_details,
create_invoke_agent_details,
create_caller_details,
create_tenant_details,
create_user_details,
create_request,
build_baggage_builder,
)
Expand All @@ -66,7 +66,8 @@
# These are used by MCP tool wrappers to access the current request's context
# without risk of concurrent request interference
_current_agent_details: contextvars.ContextVar = contextvars.ContextVar('agent_details', default=None)
_current_tenant_details: contextvars.ContextVar = contextvars.ContextVar('tenant_details', default=None)
_current_user_details: contextvars.ContextVar = contextvars.ContextVar('user_details', default=None)
_current_conversation_id: contextvars.ContextVar = contextvars.ContextVar('conversation_id', default=None)


class CrewAIAgent(AgentInterface):
Expand Down Expand Up @@ -140,7 +141,8 @@ def _create_observable_tools(self) -> list:
mcp_tools=self.mcp_tools,
tool_executor=self.mcp_tool_executor,
get_agent_details=lambda: _current_agent_details.get(),
get_tenant_details=lambda: _current_tenant_details.get(),
get_user_details=lambda: _current_user_details.get(),
get_conversation_id=lambda: _current_conversation_id.get(),
)

# =========================================================================
Expand Down Expand Up @@ -171,18 +173,18 @@ async def process_user_message(
try:
logger.info(f"Processing message: {message[:100]}...")

with build_baggage_builder(context, ctx_details.correlation_id).build():
with build_baggage_builder(context, ctx_details.conversation_id).build():
# Create observability details
agent_details = create_agent_details(ctx_details, "AI agent powered by CrewAI framework")
caller_details = create_caller_details(ctx_details)
tenant_details = create_tenant_details(ctx_details)
user_details = create_user_details(ctx_details)
request = create_request(ctx_details, message)
invoke_details = create_invoke_agent_details(ctx_details, "AI agent powered by CrewAI framework")
invoke_details = create_invoke_agent_details()

with InvokeAgentScope.start(
invoke_agent_details=invoke_details,
tenant_details=tenant_details,
request=request,
scope_details=invoke_details,
agent_details=agent_details,
caller_details=caller_details,
) as invoke_scope:
if hasattr(invoke_scope, 'record_input_messages'):
Expand All @@ -194,7 +196,8 @@ async def process_user_message(
# Store context for observable MCP tool wrappers using context variables
# This is thread/async-safe for concurrent request handling
agent_details_token = _current_agent_details.set(agent_details)
tenant_details_token = _current_tenant_details.set(tenant_details)
user_details_token = _current_user_details.set(user_details)
conversation_id_token = _current_conversation_id.set(ctx_details.conversation_id)

try:
# Create observable MCP tool wrappers
Expand All @@ -206,15 +209,16 @@ async def process_user_message(

# Run CrewAI with InferenceScope
full_response = await self._run_crew_with_inference_scope(
message, observable_mcp_tools, agent_details, tenant_details, request, display_name
message, observable_mcp_tools, agent_details, user_details, request, display_name
)

if hasattr(invoke_scope, 'record_output_messages'):
invoke_scope.record_output_messages([full_response])
finally:
# Reset context variables to previous values
_current_agent_details.reset(agent_details_token)
_current_tenant_details.reset(tenant_details_token)
_current_user_details.reset(user_details_token)
_current_conversation_id.reset(conversation_id_token)

logger.info("✅ Observability scopes closed successfully")
return full_response
Expand All @@ -225,7 +229,7 @@ async def process_user_message(
return f"Sorry, I encountered an error: {str(e)}"

async def _run_crew_with_inference_scope(
self, message: str, observable_mcp_tools: list, agent_details, tenant_details, request, user_name: str = "unknown"
self, message: str, observable_mcp_tools: list, agent_details, user_details, request, user_name: str = "unknown"
) -> str:
"""Run CrewAI with InferenceScope for LLM call tracking."""
inference_details = InferenceCallDetails(
Expand All @@ -235,10 +239,10 @@ async def _run_crew_with_inference_scope(
)

with InferenceScope.start(
request=request,
details=inference_details,
agent_details=agent_details,
tenant_details=tenant_details,
request=request,
user_details=user_details,
Comment thread
Yogeshp-MSFT marked this conversation as resolved.
) as inference_scope:
from crew_agent.agent_runner import run_crew

Expand Down
41 changes: 33 additions & 8 deletions python/crewai/sample_agent/docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,33 @@ class CrewAIAgent(AgentInterface):
```bash
# LLM Configuration (used by CrewAI)
OPENAI_API_KEY=sk-...
AZURE_API_KEY=...
AZURE_API_BASE=https://your-resource.openai.azure.com/
AZURE_API_VERSION=2025-01-01-preview
AZURE_OPENAI_DEPLOYMENT=azure/gpt-4.1
OPENAI_MODEL_NAME=azure/gpt-4.1

# Authentication
BEARER_TOKEN=...
USE_AGENTIC_AUTH=false
AUTH_HANDLER_NAME=AGENTIC
CLIENT_ID=...
TENANT_ID=...
AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default
AGENT_ID=...
AGENTIC_APP_ID=crewai-agent

# Weather Search
TAVILY_API_KEY=tvly-...

# Observability
OBSERVABILITY_SERVICE_NAME=crewai-sample-agent
OBSERVABILITY_SERVICE_NAME=crewai-agent-sample
OBSERVABILITY_SERVICE_NAMESPACE=agent365-samples
ENABLE_OBSERVABILITY=true
ENABLE_A365_OBSERVABILITY_EXPORTER=false
PYTHON_ENVIRONMENT=development

# Server
PORT=3978
LOG_LEVEL=INFO
```

## Message Flow
Expand All @@ -127,12 +145,19 @@ OBSERVABILITY_SERVICE_NAME=crewai-sample-agent
```toml
[project]
dependencies = [
"crewai>=0.30.0",
"microsoft-agents-hosting-aiohttp>=0.0.1",
"microsoft-agents-hosting-core>=0.0.1",
"microsoft_agents_a365_observability_core>=0.0.1",
"microsoft_agents_a365_tooling_core>=0.0.1",
"crewai[azure-ai-inference,tools]==1.4.1",
"tavily-python>=0.3.0",
"python-dotenv>=1.0.0",
"microsoft-agents-hosting-aiohttp>=0.9.0",
"microsoft-agents-hosting-core>=0.9.0",
"microsoft-agents-authentication-msal>=0.9.0",
"microsoft-agents-activity>=0.9.0",
"aiohttp",
"microsoft_agents_a365_tooling>=0.1.0",
"microsoft_agents_a365_observability_core>=0.1.0",
"microsoft_agents_a365_observability_hosting>=0.1.0",
"microsoft_agents_a365_notifications>=0.1.0",
"microsoft_agents_a365_runtime>=0.1.0",
]
```

Expand Down
15 changes: 8 additions & 7 deletions python/crewai/sample_agent/host_agent_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@

from turn_context_utils import (
extract_turn_context_details,
create_agent_details,
create_invoke_agent_details,
create_caller_details,
create_tenant_details,
create_user_details,
create_request,
)
from token_cache import cache_agentic_token, get_cached_agentic_token
Expand All @@ -73,7 +74,6 @@
# Enable observability SDK logging to see exporter warnings
observability_logger = logging.getLogger("microsoft_agents_a365.observability")
observability_logger.setLevel(logging.INFO)

logger = logging.getLogger(__name__)

# Load configuration
Expand Down Expand Up @@ -206,7 +206,7 @@ async def on_message(context: TurnContext, _: TurnState):
# Extract context from turn using shared utility
ctx_details = extract_turn_context_details(context)

with BaggageBuilder().tenant_id(ctx_details.tenant_id).agent_id(ctx_details.agent_id).correlation_id(ctx_details.correlation_id).build():
with BaggageBuilder().tenant_id(ctx_details.tenant_id).agent_id(ctx_details.agent_id).session_id(ctx_details.conversation_id).build():
if not self.agent_instance:
error_msg = "ERROR Sorry, the agent is not available."
logger.error(error_msg)
Expand Down Expand Up @@ -262,16 +262,17 @@ async def _typing_loop():
typing_task = asyncio.create_task(_typing_loop())
try:
# Create observability details using shared utility
invoke_details = create_invoke_agent_details(ctx_details, "AI agent powered by CrewAI framework")
invoke_details = create_invoke_agent_details()
caller_details = create_caller_details(ctx_details)
tenant_details = create_tenant_details(ctx_details)
user_details = create_user_details(ctx_details)
request = create_request(ctx_details, user_message)
agent_details = create_agent_details(ctx_details, "AI agent powered by CrewAI framework")

# Wrap the agent invocation with InvokeAgentScope
with InvokeAgentScope.start(
invoke_agent_details=invoke_details,
tenant_details=tenant_details,
request=request,
scope_details=invoke_details,
agent_details=agent_details,
caller_details=caller_details,
) as invoke_scope:
if hasattr(invoke_scope, 'record_input_messages'):
Expand Down
38 changes: 27 additions & 11 deletions python/crewai/sample_agent/mcp_observable_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from crewai.tools import BaseTool
from pydantic import BaseModel, Field, create_model

from microsoft_agents_a365.observability.core import ExecuteToolScope, ToolCallDetails
from microsoft_agents_a365.observability.core import ExecuteToolScope, ToolCallDetails, Request
from mcp_tool_registration_service import MCPToolDefinition

if TYPE_CHECKING:
Expand Down Expand Up @@ -49,7 +49,8 @@ async def call_tool(
tool_name: str,
arguments: dict,
agent_details: Any,
tenant_details: Any,
user_details: Any,
conversation_id: str | None = None,
) -> str:
"""
Call an MCP tool by name with ExecuteToolScope observability.
Expand All @@ -58,7 +59,8 @@ async def call_tool(
tool_name: Name of the tool to call
arguments: Tool arguments as a dictionary
agent_details: AgentDetails for observability
tenant_details: TenantDetails for observability
user_details: UserDetails for observability
conversation_id: Optional conversation ID for telemetry correlation

Returns:
The tool result as a string
Expand Down Expand Up @@ -86,11 +88,19 @@ async def call_tool(
endpoint=endpoint,
)

# Create a Request for the tool scope
tool_request = Request(
content=args_str,
session_id=conversation_id,
conversation_id=conversation_id,
)

# Execute with ExecuteToolScope for observability
with ExecuteToolScope.start(
Comment thread
Yogeshp-MSFT marked this conversation as resolved.
request=tool_request,
details=tool_call_details,
agent_details=agent_details,
tenant_details=tenant_details,
user_details=user_details,
) as tool_scope:
try:
logger.info(f"🔧 Calling MCP tool: {tool_name}")
Expand All @@ -112,7 +122,8 @@ def create_observable_mcp_tools(
mcp_tools: list[MCPToolDefinition],
tool_executor: MCPToolExecutor,
get_agent_details: callable,
get_tenant_details: callable,
get_user_details: callable,
get_conversation_id: callable = None,
) -> list[BaseTool]:
"""
Create CrewAI-compatible tool wrappers for MCP tools with ExecuteToolScope observability.
Expand All @@ -125,7 +136,8 @@ def create_observable_mcp_tools(
mcp_tools: List of MCP tool definitions from the registration service
tool_executor: MCPToolExecutor instance for executing tools with observability
get_agent_details: Callable that returns current agent details for observability
get_tenant_details: Callable that returns current tenant details for observability
get_user_details: Callable that returns current user details for observability
get_conversation_id: Optional callable that returns current conversation ID for telemetry

Returns:
List of CrewAI BaseTool instances that wrap MCP tools with observability
Expand All @@ -141,7 +153,7 @@ def create_observable_mcp_tools(

# Create a closure to capture the tool definition and executor reference
tool_class = _create_tool_class(
mcp_tool, InputModel, tool_executor, get_agent_details, get_tenant_details
mcp_tool, InputModel, tool_executor, get_agent_details, get_user_details, get_conversation_id
)

observable_tools.append(tool_class)
Expand Down Expand Up @@ -205,7 +217,8 @@ def _create_tool_class(
input_model: Type[BaseModel],
tool_executor: MCPToolExecutor,
get_agent_details: callable,
get_tenant_details: callable,
get_user_details: callable,
get_conversation_id: callable = None,
) -> BaseTool:
"""
Create a CrewAI BaseTool class for an MCP tool with observability.
Expand All @@ -215,7 +228,8 @@ def _create_tool_class(
input_model: Pydantic model for input validation
tool_executor: MCPToolExecutor for executing the tool
get_agent_details: Callable to get current agent details
get_tenant_details: Callable to get current tenant details
get_user_details: Callable to get current user details
get_conversation_id: Optional callable to get current conversation ID

Returns:
Instance of the created tool class
Expand All @@ -240,7 +254,8 @@ def _run(self, **kwargs) -> str:
tool_name=tool_def.name,
arguments=kwargs,
agent_details=get_agent_details(),
tenant_details=get_tenant_details(),
user_details=get_user_details(),
conversation_id=get_conversation_id() if get_conversation_id else None,
),
loop
)
Expand All @@ -253,7 +268,8 @@ def _run(self, **kwargs) -> str:
tool_name=tool_def.name,
arguments=kwargs,
agent_details=get_agent_details(),
tenant_details=get_tenant_details(),
user_details=get_user_details(),
conversation_id=get_conversation_id() if get_conversation_id else None,
)
)
return result
Expand Down
8 changes: 4 additions & 4 deletions python/crewai/sample_agent/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ dependencies = [
"python-dotenv>=1.0.0",

# Microsoft 365 Agents SDK - hosting and auth
"microsoft-agents-hosting-aiohttp>=0.7.0",
"microsoft-agents-hosting-core>=0.7.0",
"microsoft-agents-authentication-msal>=0.7.0",
"microsoft-agents-activity>=0.7.0",
"microsoft-agents-hosting-aiohttp>=0.9.0",
"microsoft-agents-hosting-core>=0.9.0",
"microsoft-agents-authentication-msal>=0.9.0",
"microsoft-agents-activity>=0.9.0",

# Core hosting deps
"aiohttp",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ weather_checker:
and forecasting tools. You excel at gathering comprehensive weather information
including temperature, precipitation, wind conditions, and visibility to help
people make informed decisions about outdoor activities and driving conditions.
llm: azure/gpt-4o

driving_safety_advisor:
role: >
Expand All @@ -21,5 +20,4 @@ driving_safety_advisor:
their performance characteristics, and safety considerations. You understand that
summer tires have reduced grip in cold temperatures (below 7°C/45°F), on wet roads,
and especially on snow or ice. You provide clear, safety-focused recommendations
based on weather data.
llm: azure/gpt-4o
based on weather data.
Loading
Loading