rich-art-45250
12/27/2025, 1:43 AMtall-airline-16756
12/27/2025, 1:43 AMtall-airline-16756
12/27/2025, 1:43 AMAgentSession and want a text-based e2e test with pytest (typical).
• You need audio pipeline e2e testing (requires external tools).
• You’re using a custom test harness or nonstandard LLM plugin.
If you’re using the typical Agents + AgentSession path, here’s a concise test pattern you can follow.
1) Test tools
• Use pytest + pytest-asyncio.
2) Start an AgentSession for the agent under test
• Create an AgentSession with your LLM plugin and start your agent inside an async context.
3) Drive the TaskGroup step-by-step
• Use await session.run(user_input=...) repeatedly to simulate each user turn that the TaskGroup expects.
• After each run, assert the expected tool/function calls or events (for example, function call names) using the session result assertions.
• Continue until the TaskGroup completes and the agent returns the summarized result.
4) Validate results and side effects
• Assert final assistant messages (content/role).
• Assert side effects or structured task results (e.g., fields in your agent’s userdata or returned task output).
• Assert the sequence/order of function calls if that matters.
Minimal test skeleton (conceptual):
import pytest
from livekit.agents import AgentSession
from livekit.plugins import openai
@pytest.mark.asyncio
async def test_agent_with_task_group():
async with openai.LLM(model="gpt-4o-mini") as llm, AgentSession(llm=llm) as session:
await session.start(YourAgent())
# trigger the task group
r = await session.run(user_input="Start the workflow")
# simulate steps expected by the TaskGroup
r = await session.run(user_input="<mailto:john@example.com|john@example.com>")
await r.expect.next_event().is_function_call(name="record_email")
r = await session.run(user_input="I live in New York")
await r.expect.next_event().is_function_call(name="record_address")
r = await session.run(user_input="That's all")
await r.expect.next_event().is_message(role="assistant").content(
"Thank you, your information has been recorded."
)
# validate stored results
assert session.agent.userdata.collected_info["email"] == "<mailto:john@example.com|john@example.com>"
Notes and alternatives
• TaskGroup behavior: tasks run sequentially, share conversation context, and return a summarized result when complete.
• TaskGroup API is experimental — keep tests flexible for API changes.
• If you need full audio e2e tests, text-only session.run() won’t cover that; use audio test tools (Bluejay/Cekura/Coval/Hamming) to simulate media.
If you want, share:
• a small snippet of your agent or the TaskGroup definition,
• what assertions you want to make (tool calls, stored data, final message),
and I’ll give a tailored test example for your code.
Sources: Testing and evaluation | LiveKit docs | Collect recording consent with tasks | LiveKit docs | Tasks and task groups | LiveKit docsrich-art-45250
12/27/2025, 1:44 AMAgentSession and want a text-based e2e test with pytest (typical).
show me hwo to do it with this, can we make it where it doesnt return a summarized result but the full contexttall-airline-16756
12/27/2025, 1:44 AMAgentSession example that drives a TaskGroup step-by-step and captures the full conversation context instead of only a summarized result.
Key idea: run the TaskGroup by supplying the user turns one-by-one with session.run(...), inspect the session results/events after each step, and read the session/agent conversation state (not only the TaskGroup summary). Many AgentSession test helpers expose event streams and the agent instance (e.g., session.agent) so you can assert messages, function calls, and the conversation buffer directly.
Example test pattern
import pytest
from livekit.agents import AgentSession
from livekit.plugins import openai
@pytest.mark.asyncio
async def test_taskgroup_returns_full_context():
async with openai.LLM(model="gpt-4o-mini") as llm, AgentSession(llm=llm) as session:
# Start your agent instance that uses a TaskGroup
await session.start(YourAgent())
# 1) Kick off the task group
res = await session.run(user_input="Start workflow")
# Expect the agent to have kicked off a task (inspect events)
await res.expect.next_event().is_message(role="assistant")
# 2) Provide inputs for each task in the group
res = await session.run(user_input="My email is <mailto:john@example.com|john@example.com>")
await res.expect.next_event().is_function_call(name="record_email")
res = await session.run(user_input="I live in New York")
await res.expect.next_event().is_function_call(name="record_address")
# 3) Finish the TaskGroup (explicit final user cue)
res = await session.run(user_input="That's all")
await res.expect.next_event().is_message(role="assistant")
# 4) Inspect the full conversation context kept by the agent
# - Many test helpers expose the agent instance and its conversation buffer.
convo = session.agent.conversation # example accessor; replace if different
# Assert the conversation contains all turns (user + assistant + function calls)
assert any("<mailto:john@example.com|john@example.com>" in m.content for m in convo.messages)
assert any("New York" in m.content for m in convo.messages)
# 5) Inspect structured data/state captured by the agent
# (e.g., agent.userdata or whatever your agent populates)
assert session.agent.userdata.collected_info["email"] == "<mailto:john@example.com|john@example.com>"
assert session.agent.userdata.collected_info["address"] == "New York"
Notes and adjustments
• Replace session.agent.conversation and convo.messages with the actual accessors your agent/testing helper exposes (some projects use session.agent.history, session.agent.chat_buffer, or similar).
• The TaskGroup normally returns a summarized result to the controller, but the underlying conversation/context remains available on the agent instance — read that state in your test to get the full context.
• If your test helper only exposes a condensed summary, inspect the event stream returned by session.run() (function-call events and assistant messages) which contain the full sequence of exchanges.
• If your agent stores task outputs in agent.userdata or another state container, assert those fields directly.
If you share the names of the session/agent attributes your code exposes for conversation history (e.g., conversation, history, chat_buffer) or paste a short snippet of YourAgent/TaskGroup, I’ll adapt the example to match your exact accessors.rich-art-45250
12/27/2025, 1:50 AMtall-airline-16756
12/27/2025, 1:50 AMAgentSession to run your real ACAScreeningAgent with TaskGroup orchestration, preserves the full conversation context, and reads agent.workflow_results (or your agent state) rather than a single summary.
Replace the placeholder names (ACAScreeningAgent, SYSTEM_PROMPT, accessors) with the actual names in your code.
import pytest
from livekit.agents import AgentSession
from livekit.plugins import openai
# Ensure your ACAScreeningAgent class hardcodes or falls back to a SYSTEM_PROMPT
# so tests don't need DB access.
@pytest.mark.asyncio
async def test_aca_agent_e2e_preserves_full_context():
async with openai.LLM(model="gpt-4o-mini") as llm, AgentSession(llm=llm) as session:
# instantiate the real agent (ensure it uses a class-constant SYSTEM_PROMPT)
agent = ACAScreeningAgent()
# start the session with the agent (this should trigger on_enter -> TaskGroup)
await session.start(agent)
# Kick off the flow (first user turn)
res = await session.run(user_input="Hello, I have an appointment")
# assert assistant responded / task started
await res.expect.next_event().is_message(role="assistant")
# Provide inputs the TaskGroup expects, step by step
res = await session.run(user_input="Yes, I consent")
await res.expect.next_event().is_function_call(name="record_consent")
res = await session.run(user_input="My name is John Doe")
await res.expect.next_event().is_function_call(name="record_identity")
res = await session.run(user_input="123 Main St, New York")
await res.expect.next_event().is_function_call(name="record_address")
res = await session.run(user_input="No other household members")
await res.expect.next_event().is_function_call(name="record_household")
res = await session.run(user_input="I am a US citizen")
await res.expect.next_event().is_function_call(name="record_citizenship")
res = await session.run(user_input="I earn $50,000 per year")
await res.expect.next_event().is_function_call(name="record_income")
# Final cue to finish the flow
res = await session.run(user_input="That's all, thank you")
await res.expect.next_event().is_message(role="assistant")
# --- Inspect full conversation context kept by the agent ---
# Replace these accessors with the ones your agent exposes:
# common names: session.agent.conversation, session.agent.history, session.agent.chat_ctx
convo = getattr(session.agent, "conversation", None) or getattr(session.agent, "history", None) or getattr(session.agent, "chat_ctx", None)
assert convo is not None, "Replace convo accessor with your agent's conversation/history property"
# Example checks: ensure user content appears in conversation messages
messages = getattr(convo, "messages", convo) # adapt if convo is a list vs object
assert any("John Doe" in getattr(m, "content", m) for m in messages)
assert any("123 Main St" in getattr(m, "content", m) for m in messages)
# --- Inspect structured workflow results stored by the agent ---
# Your agent should populate something like agent.workflow_results or agent.userdata
workflow_results = getattr(session.agent, "workflow_results", None) or getattr(session.agent, "userdata", None)
assert workflow_results is not None, "Replace workflow_results accessor with your agent's state container"
# Example assertions (adapt keys to your implementation)
assert workflow_results.get("consent", {}).get("given") is True
assert workflow_results.get("identity", {}).get("name") == "John Doe"
assert workflow_results.get("address", {}).get("street") == "123 Main St"
Notes and tips
• Ensure ACAScreeningAgent uses a class-level SYSTEM_PROMPT fallback so tests do not require DB calls.
• If your agent stores conversation differently, share the attribute names and I’ll adapt assertions.
• Use res.expect.next_event() to verify function/tool calls and assistant messages as they occur (this gives the full event stream rather than a single summary).
• Keep TaskGroup timing deterministic by providing the expected user inputs in order; TaskGroup will orchestrate transitions automatically.
If you share the actual attribute names your agent uses for conversation and workflow results (e.g., chat_ctx, history, agent.userdata, or specific keys in workflow_results), I’ll adapt the snippet to your code.rich-art-45250
12/27/2025, 2:08 AMtall-airline-16756
12/27/2025, 2:08 AMrich-art-45250
12/27/2025, 2:10 AMtall-airline-16756
12/27/2025, 2:11 AMACAScreeningAgent, SYSTEM_PROMPT fallback, and the conversation/result accessors with your actual names.
import pytest
import asyncio
from livekit.agents import AgentSession
from livekit.plugins import openai
@pytest.mark.asyncio
async def test_aca_agent_full_e2e():
async with openai.LLM(model="gpt-4o-mini") as llm, AgentSession(llm=llm) as session:
# instantiate real production agent (ensure it uses a class-level SYSTEM_PROMPT or fallback)
agent = ACAScreeningAgent()
# start the session (triggers agent.on_enter -> TaskGroup orchestration)
await session.start(agent)
# Drive the full workflow with the user turns the TaskGroup expects.
# Provide the exact user utterances the workflow expects so transitions are deterministic.
await session.run(user_input="Hello, I'm here for screening")
await session.run(user_input="Yes, I consent to proceed")
await session.run(user_input="My name is John Doe")
await session.run(user_input="123 Main St, New York")
await session.run(user_input="No other household members")
await session.run(user_input="I am a US citizen")
await session.run(user_input="I earn $50,000 per year")
await session.run(user_input="No more questions, thanks")
# Optionally wait a short time for final task callbacks to finish
await asyncio.sleep(0.2)
# --- Verify event stream as needed ---
# You can inspect the last run() result or use res.expect after each run to assert function calls.
# Example: (if you captured the last result as `res`)
# await res.expect.next_event().is_message(role="assistant")
# --- Inspect full conversation context kept by the agent ---
# Adapt these accessors to your agent implementation:
convo = getattr(session.agent, "conversation", None) or getattr(session.agent, "history", None) or getattr(session.agent, "chat_ctx", None)
assert convo is not None, "Update accessor to your agent's conversation object"
messages = getattr(convo, "messages", convo) # adapt if convo is a list or object
assert any("John Doe" in getattr(m, "content", m) for m in messages)
# --- Inspect per-task workflow results stored by the agent ---
workflow_results = getattr(session.agent, "workflow_results", None) or getattr(session.agent, "userdata", None)
assert workflow_results is not None, "Update accessor to where your agent stores task results"
# Example assertions (adapt keys to your structure)
assert workflow_results.get("consent", {}).get("given") is True
assert workflow_results.get("identity", {}).get("name") == "John Doe"
assert workflow_results.get("address", {}).get("street") == "123 Main St"
Why single AgentSession is best for true e2e
• Matches production: same entrypoint, system prompt handling, and TaskGroup orchestration.
• Context flows automatically between tasks just like real calls.
• Captures the same sequence of assistant messages, function/tool calls, and final state.
• Easier to validate timing, orchestration logic, and side effects in one run.
When to use multiple sessions
• Use separate sessions per task only if you must work around SDK session invalidation bugs or simulate isolated task runs. That pattern is a useful unit/integration test but does not mirror the live user interface.
If you want, paste the actual attribute names your agent uses for conversation and workflow results (e.g., chat_ctx, history, workflow_results, userdata) and I’ll adapt the test snippet to your exact code.rich-art-45250
12/27/2025, 2:15 AMtall-airline-16756
12/27/2025, 2:15 AMworkflow_results is populated, plus a short example you can drop into your pytest e2e.
Which approach fits you best? Pick one and I can adapt it to your exact attribute names:
• A — Await a completion future the TaskGroup or agent exposes (recommended).
• B — Poll a task/agent done() or is_finished() flag with a timeout.
• C — Listen to the AgentSession event stream for a terminal "workflow complete" event.
A — Use a completion Future (recommended)
• Make the TaskGroup or ACAScreeningAgent expose an asyncio.Future or Event that is set when orchestration completes (e.g., agent.workflow_done_fut).
• Await that future in the test with a timeout before asserting results.
Example:
# inside your test
await session.start(agent)
# drive conversation:
await session.run(user_input="...") # repeat expected turns
# wait for orchestration to finish (agent exposes a Future)
await asyncio.wait_for(agent.workflow_done_fut, timeout=10.0)
# now workflow_results will be populated
assert agent.workflow_results["consent"]["given"] is True
B — Poll a done() flag (simple if you can't add a Future)
• Have TaskGroup/agent set a boolean agent.done() when finished.
• Poll with sleep + timeout.
Example:
# after driving conversation
total_wait = 0.0
while not agent.done() and total_wait < 10.0:
await asyncio.sleep(0.25)
total_wait += 0.25
assert agent.done(), "workflow did not finish in time"
# then assert workflow_results
C — Wait for a terminal event from session.run results
• Use res.expect to detect final assistant message or a function call that indicates completion.
• Then optionally await a short grace period to let callbacks write results.
Example:
res = await session.run(user_input="That's all")
await res.expect.next_event().is_message(role="assistant") # final message seen
# small grace window for results to be written
await asyncio.sleep(0.2)
assert agent.workflow_results
Practical tips
• Use a reasonable timeout (5–30s) in CI depending on LLM latency; fail fast if exceeded.
• Prefer explicit completion signals (Future/Event) over polling when you can change the agent — it’s deterministic and clean.
• If TaskGroup runs background callbacks that write results, ensure those callbacks set the completion Future or flag after finishing writes.
• Keep small grace sleeps only for short buffer time; don’t rely on arbitrary long sleeps.
If you share how your TaskGroup currently signals completion (e.g., any existing .done() or internal future name), I’ll adapt the exact test code to your agent and attribute names.