Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"python.analysis.importFormat": "relative",
"python.languageServer": "None",
}
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@ dependencies = [
"tzlocal>=5.3.1",
"tzdata>=2025.2",
"pytest>=8.4.0",
"json_log_formatter>=1.1.1",
"pytest-asyncio>=1.0.0",
"scale-gp-beta==0.1.0a20",
"ipykernel>=6.29.5",
"openai==1.99.9", # anything higher than 1.99.9 breaks litellm - https://github.com/BerriAI/litellm/issues/13711
"cloudpickle>=3.1.1",
"datadog>=0.52.1",
"ddtrace>=3.13.0"
]
requires-python = ">= 3.12,<4"
classifiers = [
Expand Down Expand Up @@ -222,7 +225,7 @@ warn_unused_ignores = false
warn_redundant_casts = false

disallow_any_generics = true
disallow_untyped_defs = true
# disallow_untyped_defs = true
disallow_untyped_calls = true
disallow_subclassing_any = true
disallow_incomplete_defs = true
Expand All @@ -238,7 +241,7 @@ cache_fine_grained = true
# ```
# Changing this codegen to make mypy happy would increase complexity
# and would not be worth it.
disable_error_code = "func-returns-value,overload-cannot-match"
disable_error_code = "func-returns-value,overload-cannot-match,no-untyped-def"

# https://github.com/python/mypy/issues/12162
[[tool.mypy.overrides]]
Expand Down
18 changes: 18 additions & 0 deletions requirements-dev.lock
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ attrs==25.3.0
# via aiohttp
# via jsonschema
# via referencing
bytecode==0.17.0
# via ddtrace
cachetools==5.5.2
# via google-auth
certifi==2023.7.22
Expand All @@ -61,6 +63,10 @@ colorlog==6.7.0
# via nox
comm==0.2.3
# via ipykernel
datadog==0.52.1
# via agentex-sdk
ddtrace==3.15.0
# via agentex-sdk
debugpy==1.8.16
# via ipykernel
decorator==5.2.1
Expand All @@ -73,6 +79,8 @@ distro==1.8.0
# via openai
# via scale-gp
# via scale-gp-beta
envier==0.6.1
# via ddtrace
execnet==2.1.1
# via pytest-xdist
executing==2.2.0
Expand Down Expand Up @@ -120,6 +128,7 @@ idna==3.4
# via yarl
importlib-metadata==7.0.0
# via litellm
# via opentelemetry-api
iniconfig==2.0.0
# via pytest
ipykernel==6.30.1
Expand All @@ -135,6 +144,8 @@ jinja2==3.1.6
# via litellm
jiter==0.10.0
# via openai
json-log-formatter==1.1.1
# via agentex-sdk
jsonref==1.1.0
# via agentex-sdk
jsonschema==4.25.0
Expand Down Expand Up @@ -186,6 +197,8 @@ openai==1.99.9
# via openai-agents
openai-agents==0.2.7
# via agentex-sdk
opentelemetry-api==1.37.0
# via ddtrace
packaging==23.2
# via huggingface-hub
# via ipykernel
Expand All @@ -207,6 +220,7 @@ propcache==0.3.1
# via aiohttp
# via yarl
protobuf==5.29.5
# via ddtrace
# via temporalio
psutil==7.0.0
# via ipykernel
Expand Down Expand Up @@ -280,6 +294,7 @@ referencing==0.36.2
regex==2025.7.34
# via tiktoken
requests==2.32.4
# via datadog
# via huggingface-hub
# via kubernetes
# via openai-agents
Expand Down Expand Up @@ -363,6 +378,7 @@ typing-extensions==4.12.2
# via nexus-rpc
# via openai
# via openai-agents
# via opentelemetry-api
# via pydantic
# via pydantic-core
# via pyright
Expand Down Expand Up @@ -393,6 +409,8 @@ wcwidth==0.2.13
# via prompt-toolkit
websocket-client==1.8.0
# via kubernetes
wrapt==1.17.3
# via ddtrace
yarl==1.20.0
# via aiohttp
zipp==3.17.0
Expand Down
18 changes: 18 additions & 0 deletions requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ attrs==25.3.0
# via aiohttp
# via jsonschema
# via referencing
bytecode==0.17.0
# via ddtrace
cachetools==5.5.2
# via google-auth
certifi==2023.7.22
Expand All @@ -57,6 +59,10 @@ colorama==0.4.6
# via griffe
comm==0.2.3
# via ipykernel
datadog==0.52.1
# via agentex-sdk
ddtrace==3.15.0
# via agentex-sdk
debugpy==1.8.16
# via ipykernel
decorator==5.2.1
Expand All @@ -66,6 +72,8 @@ distro==1.8.0
# via openai
# via scale-gp
# via scale-gp-beta
envier==0.6.1
# via ddtrace
executing==2.2.0
# via stack-data
fastapi==0.115.14
Expand Down Expand Up @@ -109,6 +117,7 @@ idna==3.4
# via yarl
importlib-metadata==8.7.0
# via litellm
# via opentelemetry-api
iniconfig==2.1.0
# via pytest
ipykernel==6.30.1
Expand All @@ -124,6 +133,8 @@ jinja2==3.1.6
# via litellm
jiter==0.10.0
# via openai
json-log-formatter==1.1.1
# via agentex-sdk
jsonref==1.1.0
# via agentex-sdk
jsonschema==4.25.0
Expand Down Expand Up @@ -169,6 +180,8 @@ openai==1.99.9
# via openai-agents
openai-agents==0.2.7
# via agentex-sdk
opentelemetry-api==1.37.0
# via ddtrace
packaging==25.0
# via huggingface-hub
# via ipykernel
Expand All @@ -188,6 +201,7 @@ propcache==0.3.1
# via aiohttp
# via yarl
protobuf==5.29.5
# via ddtrace
# via temporalio
psutil==7.0.0
# via ipykernel
Expand Down Expand Up @@ -255,6 +269,7 @@ referencing==0.36.2
regex==2025.7.34
# via tiktoken
requests==2.32.4
# via datadog
# via huggingface-hub
# via kubernetes
# via openai-agents
Expand Down Expand Up @@ -333,6 +348,7 @@ typing-extensions==4.12.2
# via nexus-rpc
# via openai
# via openai-agents
# via opentelemetry-api
# via pydantic
# via pydantic-core
# via python-on-whales
Expand Down Expand Up @@ -360,6 +376,8 @@ wcwidth==0.2.13
# via prompt-toolkit
websocket-client==1.8.0
# via kubernetes
wrapt==1.17.3
# via ddtrace
yarl==1.20.0
# via aiohttp
zipp==3.23.0
Expand Down
1 change: 1 addition & 0 deletions src/agentex/lib/environment_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class EnvVarKeys(str, Enum):


class Environment(str, Enum):
LOCAL = "local"
DEV = "development"
STAGING = "staging"
PROD = "production"
Expand Down
20 changes: 19 additions & 1 deletion src/agentex/lib/sdk/fastacp/base/base_acp_server.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import inspect
import uuid
from datetime import datetime
from collections.abc import AsyncGenerator, Awaitable, Callable
from contextlib import asynccontextmanager
Expand All @@ -9,6 +10,7 @@
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from pydantic import TypeAdapter, ValidationError
from starlette.middleware.base import BaseHTTPMiddleware

# from agentex.lib.sdk.fastacp.types import BaseACPConfig
from agentex.lib.environment_variables import EnvironmentVariables, refreshed_environment_variables
Expand All @@ -24,7 +26,7 @@
from agentex.lib.types.json_rpc import JSONRPCError, JSONRPCRequest, JSONRPCResponse
from agentex.types.task_message_update import StreamTaskMessageFull, TaskMessageUpdate
from agentex.types.task_message_content import TaskMessageContent
from agentex.lib.utils.logging import make_logger
from agentex.lib.utils.logging import ctx_var_request_id, make_logger
from agentex.lib.utils.model_utils import BaseModel
from agentex.lib.utils.registration import register_agent
from agentex.lib.sdk.fastacp.base.constants import (
Expand All @@ -38,6 +40,20 @@
task_message_update_adapter = TypeAdapter(TaskMessageUpdate)


class RequestIDMiddleware(BaseHTTPMiddleware):
"""Middleware to extract or generate request IDs and add them to logs and response headers"""

async def dispatch(self, request: Request, call_next):
# Extract request ID from header or generate a new one if there isn't one
request_id = request.headers.get("x-request-id") or str(uuid.uuid4())
logger.info(f"Request ID: {request_id}")
# Store request ID in request state for access in handlers
ctx_var_request_id.set(request_id)
# Process request
response = await call_next(request)
return response


class BaseACPServer(FastAPI):
"""
AsyncAgentACP provides RPC-style hooks for agent events and commands asynchronously.
Expand All @@ -56,6 +72,8 @@ def __init__(self):
self.post("/api")(self._handle_jsonrpc)

# Method handlers
# this just adds a request ID to the request and response headers
self.add_middleware(RequestIDMiddleware)
self._handlers: dict[RPCMethod, Callable] = {}

@classmethod
Expand Down
78 changes: 63 additions & 15 deletions src/agentex/lib/utils/logging.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,79 @@
import logging

import contextvars
from rich.console import Console
from rich.logging import RichHandler
import json_log_formatter
import os
import ddtrace
from ddtrace import tracer

_is_datadog_configured = bool(os.environ.get("DD_AGENT_HOST"))

ctx_var_request_id = contextvars.ContextVar[str]("request_id")


class CustomJSONFormatter(json_log_formatter.JSONFormatter):
def json_record(self, message: str, extra: dict, record: logging.LogRecord) -> dict:
extra = super().json_record(message, extra, record)
extra["level"] = record.levelname
extra["name"] = record.name
extra["lineno"] = record.lineno
extra["pathname"] = record.pathname
extra["request_id"] = ctx_var_request_id.get(None)
if _is_datadog_configured:
extra["dd.trace_id"] = tracer.get_log_correlation_context().get("dd.trace_id", None) or getattr(
record, "dd.trace_id", 0
)
extra["dd.span_id"] = tracer.get_log_correlation_context().get("dd.span_id", None) or getattr(
record, "dd.span_id", 0
)
# add the env, service, and version configured for the tracer
# If tracing is not set up, then this should pull values from DD_ENV, DD_SERVICE, and DD_VERSION.
service_override = ddtrace.config.service or os.getenv("DD_SERVICE")
if service_override:
extra["dd.service"] = service_override

env_override = ddtrace.config.env or os.getenv("DD_ENV")
if env_override:
extra["dd.env"] = env_override

version_override = ddtrace.config.version or os.getenv("DD_VERSION")
if version_override:
extra["dd.version"] = version_override

def make_logger(name: str):
return extra

def make_logger(name: str) -> logging.Logger:
"""
Creates a logger object with a RichHandler to print colored text.
:param name: The name of the module to create the logger for.
:return: A logger object.
"""
# Create a console object to print colored text
console = Console()

# Create a logger object with the name of the current module
logger = logging.getLogger(name)

# Set the global log level to INFO
logger.setLevel(logging.INFO)

# Add the RichHandler to the logger to print colored text
handler = RichHandler(
console=console,
show_level=False,
show_path=False,
show_time=False,
)
logger.addHandler(handler)
environment = os.getenv("ENVIRONMENT")
if environment == "local":
console = Console()
# Add the RichHandler to the logger to print colored text
handler = RichHandler(
console=console,
show_level=False,
show_path=False,
show_time=False,
)
logger.addHandler(handler)
return logger

stream_handler = logging.StreamHandler()
if _is_datadog_configured:
stream_handler.setFormatter(CustomJSONFormatter())
else:
stream_handler.setFormatter(
logging.Formatter("%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] - %(message)s")
)

logger.addHandler(stream_handler)
# Create a logger object with the name of the current module
return logger
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading