Skip to content

feat(cli): add coach ask answering through a one-node LangGraph graph - #90

Merged
lbildzinkas merged 1 commit into
masterfrom
fm/interview-coach-walking-skeleton
Oct 9, 2026
Merged

lbildzinkas merged 1 commit into
masterfrom
fm/interview-coach-walking-skeleton

Conversation

@lbildzinkas

@lbildzinkas lbildzinkas commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Intent

This change delivers issue #5, the walking skeleton: the smallest working slice of the interview coach, kept deliberately small so every step stays easy to follow and review on its own. coach ask "What is consistent hashing?" prints an answer produced by GLM-5.3. The question passes through a LangGraph graph with a single node, and the model comes from one small factory function that reads the endpoint, model name and API key from local, git-ignored configuration (the GLM Coding Plan's OpenAI-compatible endpoint on open.bigmodel.cn). There is no search, memory or mode selection yet; those capabilities arrive as later, individually reviewable units built on this base.

The acceptance criteria: the answer comes back through the one-node graph; a missing key produces a readable error instead of a stack trace; the factory sets disabled_params for tool_choice and parallel_tool_calls (ADR-0001); tests run the graph end to end with LangChain's fake chat model without touching the network, and check the base URL, model name and disabled params the factory builds; the project's single check command passes locally and in CI; and comments link the technical references used so the change can be studied while it is reviewed.

Closes #5

What Changed

  • Adds the coach ask "<question>" command (pyproject [project.scripts] entry point): the question flows through a one-node LangGraph StateGraph to GLM-5.3 on the GLM Coding Plan's OpenAI-compatible endpoint and the answer is printed; a missing API key exits non-zero with a readable error instead of a stack trace.
  • New build_model factory builds ChatOpenAI from Settings loaded from environment variables plus an optional git-ignored .env file (template .env.example, real environment variables taking precedence), and sets disabled_params to never send tool_choice or parallel_tool_calls (ADR-0001).
  • Tests cover the CLI happy path with an injected fake model and the missing-key error, settings loading and precedence, the graph end to end with LangChain's fake chat model (no network), and the factory's base URL, model name and disabled params; README documents the command and its configuration.

Risk Assessment

✅ Low: A small, well-bounded walking skeleton that satisfies every stated acceptance criterion (one-node graph, factory with disabled_params, git-ignored config, readable missing-key error, behavioral fake-model tests, referenced comments); the only finding is an informational docstring nuance with no reachable defect.

Testing

Drove the walking skeleton as an end user against a disposable OpenAI-compatible endpoint: the CLI prints the model's answer through the one-node graph (wire-captured), configuration loads from .env with real env vars winning, a missing key exits 1 with a readable one-line error and no traceback, and the ADR-0001 disabled_params guard keeps tool_choice/parallel_tool_calls off the wire on the structured-output path where LangChain would otherwise force them; all 13 offline tests including the fake-chat-model graph e2e pass. The real GLM-5.3 answer itself could not be exercised (no API key available to this run), mitigated by proving the default endpoint is reachable and the full stack works against an equivalent OpenAI-compatible endpoint.

  • Live validation: ✅ go - 5 of 7 scenarios driven live against the product
Scenario Result Live Evidence
coach ask prints the model's answer end-to-end: CLI → config → one-node LangGraph → OpenAI-compatible endpoint → stdout ✅ pass live live-cli-happy-path.txt — exit 0, answer printed, server captured the question as a user message and returned the completion
Configured API key, base URL and default model name reach the wire (Authorization header, /chat/completions path, model glm-5.3) ✅ pass live live-cli-happy-path.txt wire capture: Bearer test-key-123, path /v1/chat/completions under the configured base URL, body model glm-5.3
Configuration loads from a git-ignored .env copied from .env.example, and a real environment variable wins over the file ✅ pass live live-env-file-and-precedence.txt — wire Authorization is Bearer file-key-456 with only .env set, Bearer os-key-789 once the env var is exported; git check-ignore .env confirms the ignore rule
Missing API key produces a readable one-line error on stderr, exit code 1, and no stack trace ✅ pass live live-missing-key.txt — 'coach: error: no API key found: set INTERVIEW_COACH_API_KEY in .env (copy it from .env.example) or export it in your shell', exit 1, zero Traceback occurrences
ADR-0001 guard (adversarial): on the structured-output path that forces tool_choice and parallel_tool_calls, the factory-built model keeps both off the wire where a plain ChatOpenAI sends them ✅ pass live live-adr0001-guard-wire.txt — factory: tools sent, tool_choice <ABSENT>, parallel_tool_calls <ABSENT>; control: tool_choice forced to the tool name and parallel_tool_calls false
The graph runs end-to-end with LangChain's fake chat model and no network (issue-mandated test design) ⏸️ untested no The prior payload marked this pass with live=false: its evidence was uv run pytest -q (13 passed), which is an offline unit-test run against LangChain's fake chat model with no network, not a drive…
A real GLM-5.3 answer from open.bigmodel.cn with the owner's GLM Coding Plan key ⏸️ untested no No GLM Coding Plan API key is available to this run: none exists in the gate worktree (.env is git-ignored and not committed, per ADR-0002), and reading the operator's real checkout/config outside the…
Evidence: Live CLI happy path: coach ask end-to-end with captured wire request
$ env -u INTERVIEW_COACH_API_KEY -u INTERVIEW_COACH_BASE_URL -u INTERVIEW_COACH_MODEL \
    INTERVIEW_COACH_API_KEY=test-key-123 INTERVIEW_COACH_BASE_URL=http://127.0.0.1:8950/v1 \
    uv run coach ask "What is consistent hashing?"   # run from an empty dir, no .env
Consistent hashing maps keys onto a ring of hash space so that when a node joins or leaves, only about 1/N of the keys remap instead of nearly all of them.
exit code: 0

== HTTP request the endpoint received ==
{"method": "POST", "path": "/v1/chat/completions", "authorization": "Bearer test-key-123", "body": {"messages": [{"content": "What is consistent hashing?", "role": "user"}], "model": "glm-5.3", "stream": false}}
Evidence: Live .env file loading and env-var precedence with wire Authorization comparison
== .env (copied from the repo's .env.example, key filled, base URL pointed at the local disposable endpoint) ==
# Copy this file to `.env` and fill in your key. `.env` is git-ignored —
# never commit real keys or personal data (ADR-0002).

# Required: your GLM Coding Plan API key (https://open.bigmodel.cn/).
INTERVIEW_COACH_API_KEY=file-key-456

# Optional overrides — the defaults are shown. A real environment variable
# with the same name wins over a value in this file.
INTERVIEW_COACH_BASE_URL=http://127.0.0.1:8950/v1
#INTERVIEW_COACH_MODEL=glm-5.3

== run 1: no INTERVIEW_COACH_* env vars at all - config comes only from .env ==
$ env -u INTERVIEW_COACH_API_KEY -u INTERVIEW_COACH_BASE_URL -u INTERVIEW_COACH_MODEL uv run coach ask "Question via env file?"
Consistent hashing maps keys onto a ring of hash space so that when a node joins or leaves, only about 1/N of the keys remap instead of nearly all of them.
exit code: 0

== HTTP request received ==
{"method": "POST", "path": "/v1/chat/completions", "authorization": "Bearer file-key-456", "body": {"messages": [{"content": "Question via env file?", "role": "user"}], "model": "glm-5.3", "stream": false}}

== run 2: real env var INTERVIEW_COACH_API_KEY=os-key-789 set - it must win over .env ==
$ env -u INTERVIEW_COACH_BASE_URL -u INTERVIEW_COACH_MODEL INTERVIEW_COACH_API_KEY=os-key-789 uv run coach ask "Precedence?"
Consistent hashing maps keys onto a ring of hash space so that when a node joins or leaves, only about 1/N of the keys remap instead of nearly all of them.
exit code: 0

== HTTP request received ==
{"method": "POST", "path": "/v1/chat/completions", "authorization": "Bearer os-key-789", "body": {"messages": [{"content": "Precedence?", "role": "user"}], "model": "glm-5.3", "stream": false}}
Evidence: Live missing-key error: readable one-liner, exit 1, no traceback
$ cd <empty-dir-without-.env> && env -u INTERVIEW_COACH_API_KEY -u INTERVIEW_COACH_BASE_URL -u INTERVIEW_COACH_MODEL uv run coach ask "What is consistent hashing?"
coach: error: no API key found: set INTERVIEW_COACH_API_KEY in .env (copy it from .env.example) or export it in your shell
exit code: 1

stdout was empty; 'Traceback' appears in output: 0 time(s)
Evidence: Live ADR-0001 guard on the wire: factory vs control structured-output requests
ADR-0001 guard on the wire: with_structured_output(method='function_calling') —
the path LangChain would otherwise force tool_choice=<tool name> (rejected by GLM) and parallel_tool_calls on.

factory model (build_model, disabled_params set):
  tools sent:                 True
  tool_choice on wire:        <ABSENT>
  parallel_tool_calls on wire:<ABSENT>
control (plain ChatOpenAI, no disabled_params):
  tools sent:                 True
  tool_choice on wire:        {'type': 'function', 'function': {'name': 'Answer'}}
  parallel_tool_calls on wire:False
- Outcome: ⚠️ 1 info across 1 run (10m45s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ src/interview_coach/model.py:20 - The docstring claims disabled_params "drops request parameters before they are sent" and that therefore "neither may ever reach the endpoint", naming agent runtimes that add parallel_tool_calls when binding tools. In the pinned langchain-openai 1.6.6, disabled_params is only consulted inside with_structured_output (BaseChatOpenAI._filter_disabled_params, base.py:2781); kwargs passed through bind_tools(...) (base.py:2538-2587) or directly at invoke(...) flow unfiltered into the request payload, and the field's own docstring carries exactly this caveat ("this does not prevent a user from directly passing in the parameter during invocation"). No reachable defect exists in this change — nothing here binds tools or passes those params, so the acceptance criterion (factory sets disabled_params) is satisfied — but this repo's comments are the owner's stated learning material, and this one asserts an absolute safety property of the ADR-0001 mechanism that the pinned library version does not provide on the very bind_tools path it cites. Remedy: reword to the library's actual guarantee (filters params LangChain adds itself, e.g. in with_structured_output; explicitly passed bind/invoke params still go through), so the future agent-runtime PR does not inherit a false sense of safety. No other site in the change repeats this claim.
⚠️ **Test** - 1 info
  • ℹ️ src/interview_coach/model.py:25 - Scope boundary of the ADR-0001 guard, verified on the wire: in langchain-openai 1.6.6 disabled_params filters the kwargs with_structured_output passes to bind_tools (so the factory model keeps tool_choice/parallel_tool_calls off the wire there, while a plain ChatOpenAI control sends tool_choice: {function: {name: ...}} and parallel_tool_calls: false), but it does NOT strip params a caller supplies directly to bind_tools(..., tool_choice=&#39;auto&#39;, parallel_tool_calls=True) — those still reach the wire even on factory-built models. Not a defect against issue Walking skeleton: ask the coach one question from the terminal #5 (nothing binds tools yet, and GLM accepts tool_choice: &#39;auto&#39; per ADR-0001), but worth knowing when the agent iterations start binding tools.
  • Live validation: ✅ go - 5 of 7 scenarios driven live against the product
Scenario Result Live Evidence
coach ask prints the model's answer end-to-end: CLI → config → one-node LangGraph → OpenAI-compatible endpoint → stdout ✅ pass live live-cli-happy-path.txt — exit 0, answer printed, server captured the question as a user message and returned the completion
Configured API key, base URL and default model name reach the wire (Authorization header, /chat/completions path, model glm-5.3) ✅ pass live live-cli-happy-path.txt wire capture: Bearer test-key-123, path /v1/chat/completions under the configured base URL, body model glm-5.3
Configuration loads from a git-ignored .env copied from .env.example, and a real environment variable wins over the file ✅ pass live live-env-file-and-precedence.txt — wire Authorization is Bearer file-key-456 with only .env set, Bearer os-key-789 once the env var is exported; git check-ignore .env confirms the ignore rule
Missing API key produces a readable one-line error on stderr, exit code 1, and no stack trace ✅ pass live live-missing-key.txt — 'coach: error: no API key found: set INTERVIEW_COACH_API_KEY in .env (copy it from .env.example) or export it in your shell', exit 1, zero Traceback occurrences
ADR-0001 guard (adversarial): on the structured-output path that forces tool_choice and parallel_tool_calls, the factory-built model keeps both off the wire where a plain ChatOpenAI sends them ✅ pass live live-adr0001-guard-wire.txt — factory: tools sent, tool_choice <ABSENT>, parallel_tool_calls <ABSENT>; control: tool_choice forced to the tool name and parallel_tool_calls false
The graph runs end-to-end with LangChain's fake chat model and no network (issue-mandated test design) ⏸️ untested no The prior payload marked this pass with live=false: its evidence was uv run pytest -q (13 passed), which is an offline unit-test run against LangChain's fake chat model with no network, not a drive…
A real GLM-5.3 answer from open.bigmodel.cn with the owner's GLM Coding Plan key ⏸️ untested no No GLM Coding Plan API key is available to this run: none exists in the gate worktree (.env is git-ignored and not committed, per ADR-0002), and reading the operator's real checkout/config outside the…
  • uv run pytest -q — all 13 tests pass, including the graph run end-to-end with LangChain's fake chat model, factory base-URL/model/disabled-params checks, config loader, and CLI tests
  • uv run coach ask &#34;What is consistent hashing?&#34; from an isolated dir with env-var config against a disposable local OpenAI-compatible endpoint — exit 0, answer printed, wire request captured (POST /v1/chat/completions, Bearer key, model glm-5.3, user message, no tool_choice/parallel_tool_calls)
  • .env-file run of uv run coach ask (template copied from the repo's .env.example, key and base URL filled) then an env-var-precedence run — wire Authorization switched from Bearer file-key-456 to Bearer os-key-789
  • missing-key run of uv run coach ask in a dir with no .env and no env vars — exit 1, one readable stderr line naming INTERVIEW_COACH_API_KEY and .env.example, empty stdout, zero Traceback occurrences
  • with_structured_output(method=&#34;function_calling&#34;) wire check: factory model (disabled_params) vs plain ChatOpenAI control — factory sends tools unforced with both params absent, control sends forced tool_choice and parallel_tool_calls
  • probed the default endpoint https://open.bigmodel.cn/api/coding/paas/v4 with a dummy key — reachable, returns a proper OpenAI-style 401 (no credential available for a real answer)
  • git check-ignore .env — .env is git-ignored per ADR-0002
  • teardown: killed the disposable endpoint, removed tmp_live/, verified a clean worktree
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Implements issue #5: a coach CLI whose ask subcommand sends one question
through a one-node LangGraph StateGraph that calls GLM-5.3 on the GLM
Coding Plan's OpenAI-compatible endpoint, built by one small factory
(config in git-ignored .env / environment variables, readable error on a
missing key, disabled_params per ADR-0001). Tests run the graph end to
end with LangChain's fake chat model, no network.
@lbildzinkas
lbildzinkas merged commit acb64a0 into master Oct 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Walking skeleton: ask the coach one question from the terminal

1 participant