Download tests/manual_e2e_langsmith.md from towardsai-tutors/ai-tutor-chatbot: direct link, hf CLI and curl.
- Browser
- Download file 7.83 kB
-
https://huggingface.co/spaces/towardsai-tutors/ai-tutor-chatbot/resolve/1a2b802acfecc2ab6a1e56d795540fa60acfdd8a/tests/manual_e2e_langsmith.md
- Command line
-
hf download hf://spaces/towardsai-tutors/ai-tutor-chatbot@1a2b802acfecc2ab6a1e56d795540fa60acfdd8a/tests/manual_e2e_langsmith.md
-
curl -L -o manual_e2e_langsmith.md https://huggingface.co/spaces/towardsai-tutors/ai-tutor-chatbot/resolve/1a2b802acfecc2ab6a1e56d795540fa60acfdd8a/tests/manual_e2e_langsmith.md
Manual End-to-End LangSmith Runbook
This runbook verifies the live chatbot path end to end and then looks up the matching LangSmith trace/run from the CLI.
Use it when changing agent tools, KB browsing, citation handling, streaming API parts, or model/provider settings.
Prerequisites
Run commands from the repository root.
Required local artifacts and environment:
.envcontainsCOHERE_API_KEY.envcontainsDEEPSEEK_API_KEY.envcontainsGEMINI_API_KEYorGOOGLE_API_KEY.envcontainsLANGSMITH_API_KEY.envhasLANGSMITH_TRACING=true.envhasLANGSMITH_PROJECT=ai-tutor-appdata/chroma-db-all_sources/existsdata/kb/wiki/index.mdexistscurl,jq, and thelangsmithCLI are available throughuv run
Quick check:
uv run dotenv -f .env run -- python - <<'PY'
import os
for key in [
"COHERE_API_KEY",
"DEEPSEEK_API_KEY",
"GEMINI_API_KEY",
"GOOGLE_API_KEY",
"LANGSMITH_API_KEY",
"LANGSMITH_TRACING",
"LANGSMITH_PROJECT",
]:
value = os.getenv(key)
print(f"{key}={'set' if value else 'missing'}")
PY
Fast Automated Smoke Tests
These tests run a live model-backed request and check the frontend/API stream shape.
API stream contract:
uv run dotenv -f .env run -- env RUN_LIVE_API_E2E=1 \
pytest tests/test_api.py::test_live_api_stream_exposes_frontend_parts -q
Manual API Test
Start the FastAPI app in one terminal:
uv run dotenv -f .env run -- env \
AI_TUTOR_API_HOST=127.0.0.1 \
AI_TUTOR_API_PORT=8000 \
python -m app.api
From another terminal, confirm the server is ready:
curl -s http://127.0.0.1:8000/healthz
Create a request payload. This prompt intentionally asks for both structured retrieval and KB shell browsing so both citation paths are exercised.
cat >/tmp/ai_tutor_e2e_payload.json <<'JSON'
{
"messages": [
{
"role": "user",
"parts": [
{
"type": "text",
"text": "Use run_kb_command to answer: can you tell me about codex?"
}
]
}
],
"sourceKeys": [
"agentic_ai_engineering",
"langchain",
"langgraph",
"peft",
"transformers"
],
"enabledTools": [],
"model": "deepseek:deepseek-v4-flash",
"includeReasoning": true,
"threadId": ""
}
JSON
On enabledTools: it is an explicit allowlist of the toggle tools for the turn.
[] (as above) disables web search and URL reading, so this run exercises only
the corpus retrieval + KB shell citation paths. Two things to know:
url_contextdefaults to off in the UI (active: Falsein_tool_catalog,app/api.py), so a browser turn only sends it when the user opts in. Enabling it flipskeep_unresolved_sources=Truefor that turn, which surfaces cited http(s) URLs that match no evidence as low-trust Web chips (group="web"). With it off, such URLs are dropped from the source cards (they still render as plain links in the answer prose).- Omitting
enabledToolsentirely is not the same as[]. A direct API caller that drops the field falls through to a server default that enables every toggle tool regardless of the UIactiveflag (includingurl_context). Send an explicit list to control this; seebuild_chat_requestinapp/api.py.
Send the request and save the server-sent event stream:
curl -sN --max-time 240 http://127.0.0.1:8000/api/chat \
-H "Content-Type: application/json" \
-d @/tmp/ai_tutor_e2e_payload.json \
-o /tmp/ai_tutor_e2e_api.sse
Convert the stream to JSONL parts:
grep '^data: ' /tmp/ai_tutor_e2e_api.sse \
| sed 's/^data: //' \
| grep -v '^\[DONE\]$' \
> /tmp/ai_tutor_e2e_api.parts.jsonl
Inspect what the frontend received:
jq -r '.type' /tmp/ai_tutor_e2e_api.parts.jsonl | sort | uniq -c
Inspect tool calls:
jq -r '
select(.type == "tool-input-available" or .type == "tool-input-start")
| [.type, .toolName, ((.input // .inputText // "") | tostring | gsub("\n"; " ") | .[0:220])]
| @tsv
' /tmp/ai_tutor_e2e_api.parts.jsonl
Inspect final answer text:
jq -r 'select(.type == "text-delta") | .delta' \
/tmp/ai_tutor_e2e_api.parts.jsonl \
| tr -d '\n'
echo
Inspect source cards:
jq -r '
select(.type == "data-source")
| [.data.title, .data.url, .data.sourceKey, (.data.score // "")]
| @tsv
' /tmp/ai_tutor_e2e_api.parts.jsonl
Expected result:
- The stream includes
tool-input-*,tool-output-available,text-delta,source-url,source-document,data-source, andfinishparts. - Tool calls include
retrieve_tutor_contextandrun_kb_command. - The answer has inline citations, not only a final sources list.
data-sourceparts include the source cards the frontend will render.
Find The LangSmith Trace
List recent traces in the project:
uv run dotenv -f .env run -- langsmith trace list \
--project ai-tutor-app \
--limit 10 \
--full \
--format json \
-o /tmp/ai_tutor_langsmith_traces.json
Print the likely match. Look for the newest trace whose input contains the manual prompt.
jq -r '
.[]
| [
(.trace_id // .id),
(.status // ""),
(.duration_ms // .latency_ms // .latency // ""),
((.inputs.query // .inputs.input // .inputs.messages[-1].content // .inputs // "") | tostring | gsub("\n"; " ") | .[0:220])
]
| @tsv
' /tmp/ai_tutor_langsmith_traces.json
Save the trace ID:
TRACE_ID="paste-trace-id-here"
Fetch the full trace:
uv run dotenv -f .env run -- langsmith trace get "$TRACE_ID" \
--project ai-tutor-app \
--full \
--format json \
-o "/tmp/ai_tutor_trace_${TRACE_ID}.json"
Fetch the corresponding run. For the root run, the trace ID is usually also the run ID. If this fails, open the trace JSON and use the root run ID shown there.
uv run dotenv -f .env run -- langsmith run get "$TRACE_ID" \
--project ai-tutor-app \
--full \
--format json \
-o "/tmp/ai_tutor_run_${TRACE_ID}.json"
Inspect Trace/Run Details
Overall status and latency:
jq '{
id: (.trace_id // .id),
name,
status,
duration_ms: (.duration_ms // .latency_ms // .latency),
run_count: (.run_count // ((.runs // .child_runs // []) | length))
}' "/tmp/ai_tutor_trace_${TRACE_ID}.json"
Tool calls:
jq -r '
.. | objects
| select((.name? // "") | test("retrieve_tutor_context|run_kb_command"))
| [
(.name // ""),
(.status // ""),
(.duration_ms // .latency_ms // .latency // ""),
((.inputs.command // .inputs.query // .inputs.input // "") | tostring | gsub("\n"; " ") | .[0:220])
]
| @tsv
' "/tmp/ai_tutor_trace_${TRACE_ID}.json"
LLM calls:
jq -r '
.. | objects
| select((.run_type? // "") | test("llm|chat_model"))
| [
(.name // ""),
(.status // ""),
(.duration_ms // .latency_ms // .latency // ""),
(.prompt_tokens // .usage.prompt_tokens // .outputs.llm_output.token_usage.prompt_tokens // ""),
(.completion_tokens // .usage.completion_tokens // .outputs.llm_output.token_usage.completion_tokens // "")
]
| @tsv
' "/tmp/ai_tutor_trace_${TRACE_ID}.json"
Search the raw trace for the KB shell command sequence:
rg -n "run_kb_command|retrieve_tutor_context|sed -n|rg -n|cat wiki" \
"/tmp/ai_tutor_trace_${TRACE_ID}.json"
For latency debugging, compare these numbers:
- Root trace duration.
- Count of
run_kb_commandcalls. - Total shell/tool duration.
- Count of LLM/chat model calls.
- Longest LLM call duration.
- Whether the trace ended in
success,error, or cancellation.
If shell/tool duration is tiny but total trace duration is large, the bottleneck is model inference or repeated model turns, not filesystem access.