Files
letta-server/tests/test_provider_trace.py
Kian Jones 45c4dbd5e8 chore(ci): Add uv support and use for unit tests (#4127)
* cherrypick just relevant commits?

* make work with poetry

* update poetry?

* regen?

* change tests and dev to dependency groups instead of optional extras

* Fix Poetry/UV compatibility issues

- Fix sqlite-vec dependency: Remove optional flag from Poetry section to match main deps
- Regenerate poetry.lock to sync with pyproject.toml changes
- Test both package managers successfully:
  - Poetry: `poetry install --with dev --with test -E postgres -E external-tools -E cloud-tool-sandbox`
  - UV: `uv sync --group dev --group test --extra postgres --extra external-tools --extra cloud-tool-sandbox`

Resolves Poetry lock sync errors and ensures sqlite-vec is available for tests.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* more robust pip install

* Fix fern SDK wheel installation in CI workflow

Replace unreliable command substitution with proper error handling:
- Check if directory exists before attempting to find wheels
- Store wheel file path in variable to avoid empty arguments
- Provide clear error messages when directory/wheels are missing
- Prevents "required arguments were not provided" error in uv pip install

Fixes: error: the following required arguments were not provided: <PACKAGE>

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* debugging

* trigger CI

* ls

* revert whl installation to -e

* programmatic HIT version insertion

* version templating properly

* set var properly

* labelling

* remove version insertion

* ?

* try using sed '2r /dev/stdin'

* version

* try again smh

* not trigger on poetry version

* only add once

* filter only for project not poetry

* hand re-construct the file

* save tail?

* fix docker command

* please please please

* rename test -> tests

* update poetry and rename group to -E

* move async into tests extra and regen lock files and add sqlite extra

* remove loading cached venv from cloud api integration

* add uv dependency to CI runners

* test removing the custom event loop

* regen poetry.lock and try to fix async tests

* wrap async pg exception and event loop tweak in plugins

* remove event loop from plugins test and remove caching from cloud-api-integration-test

* migrate all tests away from event loop for pytest-asyncio

* pin firecrawl

* pin e2b

* take claude's suggestion

* deeper down the claude rabbit hole

* increase timeout for httpbin.org

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-26 11:51:31 -07:00

210 lines
7.6 KiB
Python

import asyncio
import json
import os
import threading
import time
import uuid
import pytest
from dotenv import load_dotenv
from letta_client import Letta
from letta.agents.letta_agent import LettaAgent
from letta.schemas.embedding_config import EmbeddingConfig
from letta.schemas.letta_message_content import TextContent
from letta.schemas.llm_config import LLMConfig
from letta.schemas.message import MessageCreate
from letta.server.rest_api.streaming_response import StreamingResponseWithStatusCode
from letta.services.agent_manager import AgentManager
from letta.services.block_manager import BlockManager
from letta.services.job_manager import JobManager
from letta.services.message_manager import MessageManager
from letta.services.passage_manager import PassageManager
from letta.services.step_manager import StepManager
from letta.services.telemetry_manager import NoopTelemetryManager, TelemetryManager
def _run_server():
"""Starts the Letta server in a background thread."""
load_dotenv()
from letta.server.rest_api.app import start_server
start_server(debug=True)
@pytest.fixture(scope="session")
def server_url():
"""Ensures a server is running and returns its base URL."""
url = os.getenv("LETTA_SERVER_URL", "http://localhost:8283")
if not os.getenv("LETTA_SERVER_URL"):
thread = threading.Thread(target=_run_server, daemon=True)
thread.start()
time.sleep(5) # Allow server startup time
return url
# # --- Client Setup --- #
@pytest.fixture(scope="session")
def client(server_url):
"""Creates a REST client for testing."""
client = Letta(base_url=server_url)
yield client
@pytest.fixture(scope="session")
def event_loop(request):
"""Create an instance of the default event loop for each test case."""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest.fixture(scope="function")
def roll_dice_tool(client, roll_dice_tool_func):
print_tool = client.tools.upsert_from_function(func=roll_dice_tool_func)
yield print_tool
@pytest.fixture(scope="function")
def weather_tool(client, weather_tool_func):
weather_tool = client.tools.upsert_from_function(func=weather_tool_func)
yield weather_tool
@pytest.fixture(scope="function")
def print_tool(client, print_tool_func):
print_tool = client.tools.upsert_from_function(func=print_tool_func)
yield print_tool
@pytest.fixture(scope="function")
def agent_state(client, roll_dice_tool, weather_tool):
"""Creates an agent and ensures cleanup after tests."""
agent_state = client.agents.create(
name=f"test_compl_{str(uuid.uuid4())[5:]}",
tool_ids=[roll_dice_tool.id, weather_tool.id],
include_base_tools=True,
memory_blocks=[
{
"label": "human",
"value": "Name: Matt",
},
{
"label": "persona",
"value": "Friendly agent",
},
],
llm_config=LLMConfig.default_config(model_name="gpt-4o-mini"),
embedding_config=EmbeddingConfig.default_config(provider="openai"),
)
yield agent_state
client.agents.delete(agent_state.id)
@pytest.mark.asyncio
@pytest.mark.parametrize("message", ["Get the weather in San Francisco."])
async def test_provider_trace_experimental_step(message, agent_state, default_user):
experimental_agent = LettaAgent(
agent_id=agent_state.id,
message_manager=MessageManager(),
agent_manager=AgentManager(),
block_manager=BlockManager(),
job_manager=JobManager(),
passage_manager=PassageManager(),
step_manager=StepManager(),
telemetry_manager=TelemetryManager(),
actor=default_user,
)
response = await experimental_agent.step([MessageCreate(role="user", content=[TextContent(text=message)])])
tool_step = response.messages[0].step_id
reply_step = response.messages[-1].step_id
tool_telemetry = await experimental_agent.telemetry_manager.get_provider_trace_by_step_id_async(step_id=tool_step, actor=default_user)
reply_telemetry = await experimental_agent.telemetry_manager.get_provider_trace_by_step_id_async(step_id=reply_step, actor=default_user)
assert tool_telemetry.request_json
assert reply_telemetry.request_json
@pytest.mark.asyncio
@pytest.mark.parametrize("message", ["Get the weather in San Francisco."])
async def test_provider_trace_experimental_step_stream(message, agent_state, default_user):
experimental_agent = LettaAgent(
agent_id=agent_state.id,
message_manager=MessageManager(),
agent_manager=AgentManager(),
block_manager=BlockManager(),
job_manager=JobManager(),
passage_manager=PassageManager(),
step_manager=StepManager(),
telemetry_manager=TelemetryManager(),
actor=default_user,
)
stream = experimental_agent.step_stream([MessageCreate(role="user", content=[TextContent(text=message)])])
result = StreamingResponseWithStatusCode(
stream,
media_type="text/event-stream",
)
message_id = None
async def test_send(message) -> None:
nonlocal message_id
if "body" in message and not message_id:
body = message["body"].decode("utf-8").split("data:")
message_id = json.loads(body[1])["id"]
await result.stream_response(send=test_send)
messages = await experimental_agent.message_manager.get_messages_by_ids_async([message_id], actor=default_user)
step_ids = set((message.step_id for message in messages))
for step_id in step_ids:
telemetry_data = await experimental_agent.telemetry_manager.get_provider_trace_by_step_id_async(step_id=step_id, actor=default_user)
assert telemetry_data.request_json
assert telemetry_data.response_json
@pytest.mark.asyncio
@pytest.mark.parametrize("message", ["Get the weather in San Francisco."])
async def test_provider_trace_step(client, agent_state, default_user, message):
client.agents.messages.create(agent_id=agent_state.id, messages=[])
response = client.agents.messages.create(
agent_id=agent_state.id,
messages=[MessageCreate(role="user", content=[TextContent(text=message)])],
)
tool_step = response.messages[0].step_id
reply_step = response.messages[-1].step_id
tool_telemetry = await TelemetryManager().get_provider_trace_by_step_id_async(step_id=tool_step, actor=default_user)
reply_telemetry = await TelemetryManager().get_provider_trace_by_step_id_async(step_id=reply_step, actor=default_user)
assert tool_telemetry.request_json
assert reply_telemetry.request_json
@pytest.mark.asyncio
@pytest.mark.parametrize("message", ["Get the weather in San Francisco."])
async def test_noop_provider_trace(message, agent_state, default_user):
experimental_agent = LettaAgent(
agent_id=agent_state.id,
message_manager=MessageManager(),
agent_manager=AgentManager(),
block_manager=BlockManager(),
job_manager=JobManager(),
passage_manager=PassageManager(),
step_manager=StepManager(),
telemetry_manager=NoopTelemetryManager(),
actor=default_user,
)
response = await experimental_agent.step([MessageCreate(role="user", content=[TextContent(text=message)])])
tool_step = response.messages[0].step_id
reply_step = response.messages[-1].step_id
tool_telemetry = await experimental_agent.telemetry_manager.get_provider_trace_by_step_id_async(step_id=tool_step, actor=default_user)
reply_telemetry = await experimental_agent.telemetry_manager.get_provider_trace_by_step_id_async(step_id=reply_step, actor=default_user)
assert tool_telemetry is None
assert reply_telemetry is None