Co-consult
Follow a travel request through intake, parallel flight/hotel/activity searches, advisory research and an evidence-checked proposal. Follow-up chat reuses saved searches. The example registers 31 capabilities under co-consult, version 0.3.0, and includes an installable node manifest with entrypoint node:app. This is application logic built on the shared runtime.
Capabilities
| Capability | Kind | Signature | Module | Summary |
|---|---|---|---|---|
activities_desk | traversal | (destination: str, party: str, preferences: str = '', model: str | None = None) -> dict | examples/python/co_consult/desks/activities.py | The activities track: frame the query, search the catalogue (twice at most), shortlist for this party. |
activity_search | skill | (query: str) -> dict | examples/python/co_consult/desks/activities.py | Semantic search over the activities catalogue (fixture, or LIVN vector search in live mode). Returns {shop_id, activities, counts} or {error}. |
frame_activity_query | traversal | (destination: str, party: str, preferences: str, model: str | None = None) -> dict | examples/python/co_consult/desks/activities.py | Given destination, party and interests, write a primary and an alternate semantic query. |
rank_activities | traversal | (activities: list[dict], party: str, preferences: str, model: str | None = None) -> dict | examples/python/co_consult/desks/activities.py | Given the deduplicated catalogue hits, shortlist up to six with fit notes. |
context_desk | traversal | (situation: str, route: dict, dates: dict, adults: int, currency: str, model: str | None = None) -> dict | examples/python/co_consult/desks/context.py | The advisory track: plan the lookups this proposal needs, run them in parallel, summarise each. |
context_lookup | skill | (engine: str, query: dict) -> dict | examples/python/co_consult/desks/context.py | One advisory lookup (fare_insights | destination_context | map_context | web_context | reviews_context); fixture or SerpAPI. |
plan_context_lookups | decision | (situation: str, model: str | None = None) -> dict | examples/python/co_consult/desks/context.py | Given what the suppliers returned, decide which advisory lookups to run and phrase each question. |
summarise_lookup | traversal | (engine: str, question: str, result: dict, model: str | None = None) -> dict | examples/python/co_consult/desks/context.py | Given one raw lookup result, answer the question in 2-3 sentences with the source named. |
flight_shop | skill | (legs: list[dict], travelers: list[str]) -> dict | examples/python/co_consult/desks/flights.py | Run a flight shop (fixture, or Sabre Flight Shop in live mode). Returns {shop_id, offers, counts} or {error, detail}. |
flights_desk | traversal | (legs: list[dict], travelers: list[str], party: str, preferences: str = '', model: str | None = None) -> dict | examples/python/co_consult/desks/flights.py | The flights track: shop, rank for this party, read the rules on the top picks. |
interpret_fare_rules | traversal | (shop_id: str, offer_id: str, party: str, model: str | None = None) -> dict | examples/python/co_consult/desks/flights.py | Given a stored offer, return refundability, change policy and baggage in plain language. |
offer_detail | skill | (shop_id: str, offer_id: str) -> dict | examples/python/co_consult/desks/flights.py | Resolve one offer's itinerary, fare rules and baggage from a stored shop. Never re-searches. |
rank_flight_offers | traversal | (offers: list[dict], party: str, preferences: str, model: str | None = None) -> dict | examples/python/co_consult/desks/flights.py | Given the shortlist and the party, pick the recommended offer and a runner-up with a reason. |
assess_property | traversal | (property_token: str, search: dict, party: str, preferences: str, model: str | None = None) -> dict | examples/python/co_consult/desks/hotels.py | Given one property's full detail, judge its fit for this party. |
hotel_detail | skill | (shop_id: str, property_token: str, location: str, check_in_date: str, check_out_date: str, adults: int = 2, children_ages: str = '', currency: str = 'AUD', country: str = 'au') -> dict | examples/python/co_consult/desks/hotels.py | Resolve one property by its token. Dates, party and location must match the saved search. |
hotel_search | skill | (location: str, check_in_date: str, check_out_date: str, adults: int = 2, children_ages: str = '', currency: str = 'AUD', country: str = 'au') -> dict | examples/python/co_consult/desks/hotels.py | Search hotels for a destination and dates (fixture, or SerpAPI google_hotels in live mode). Returns {shop_id, properties, counts} or {error}. |
hotels_desk | traversal | (location: str, check_in_date: str, check_out_date: str, adults: int, children_ages: str, party: str, currency: str = 'AUD', country: str = 'au', budget_note: str = '', preferences: str = '', model: str | None = None) -> dict | examples/python/co_consult/desks/hotels.py | The hotels track: search, shortlist for this party, assess each pick in full. |
rank_hotel_shortlist | traversal | (properties: list[dict], party: str, budget_note: str, preferences: str, currency: str, model: str | None = None) -> dict | examples/python/co_consult/desks/hotels.py | Given the search results and the party, pick up to three properties with a reason each. |
extract_brief | traversal | (request: str, today: str, model: str | None = None) -> dict | examples/python/co_consult/desks/intake.py | Given the traveller's sentence, return the structured facts it contains. |
intake_brief | traversal | (request: str, origin: str = '', destination: str = '', depart_date: str = '', return_date: str = '', adults: int = 0, children_ages: str = '', children_count: int = 0, budget: str = '', preferences: str = '', currency: str = '', model: str | None = None, known: dict | None = None) -> dict | examples/python/co_consult/desks/intake.py | Normalize a request into a brief and report whether it is workable (status ready | needs_requirements). |
resolve_airport | traversal | (place: str, model: str | None = None) -> dict | examples/python/co_consult/desks/intake.py | Given a city or region, return its main international airport IATA code (or unconfident). |
resolve_route | traversal | (brief: dict, model: str | None = None) -> dict | examples/python/co_consult/desks/intake.py | Turn a ready brief into flight legs (IATA, travel order) and passenger-type codes. |
budget_fit | traversal | (budget_amount: float, currency: str, totals: dict, model: str | None = None) -> dict | examples/python/co_consult/desks/proposal.py | Compare like currencies in code; an incomplete subtotal is never a full-trip quote. |
build_proposal | skill | (selection: dict, brief: dict, sections: dict, gaps: list[str], advisory: list[dict], budget: dict, provenance: dict) -> dict | examples/python/co_consult/desks/proposal.py | Assemble the reviewable proposal document from checked evidence. |
compose_recommendation | traversal | (material: str, model: str | None = None) -> dict | examples/python/co_consult/desks/proposal.py | Write the proposal sections from supplied evidence only. |
propose | traversal | (brief: dict, route: dict, flights: dict, hotels: dict, activities: dict, context: dict, selections: dict | None = None, model: str | None = None) -> dict | examples/python/co_consult/desks/proposal.py | Select, provenance-check, budget-check, compose and build the proposal. |
provenance_check | skill | (selection: dict) -> dict | examples/python/co_consult/desks/proposal.py | Check item membership AND quoted fields against this session's raw records. |
answer_saved | traversal | (message: str, evidence: dict, model: str | None = None) -> dict | examples/python/co_consult/main.py | No supplier tools: answer only from saved, session-scoped evidence. |
chat | traversal | (message: str, session_id: str | None = None, hints: dict | None = None, request_id: str | None = None, refresh: bool = False, new_trip: bool = False, shop_id: str | None = None, selections: dict | None = None, model: str | None = None, country: str = '', nights: int = 0) -> ChatResponse | examples/python/co_consult/main.py | Keep a brief and proposal across turns; only refresh=true repeats supplier shopping. |
consult | traversal | (request: str, origin: str = '', destination: str = '', depart_date: str = '', return_date: str = '', adults: int = 0, children_ages: str = '', children_count: int = 0, budget: str = '', preferences: str = '', currency: str = '', country: str = '', nights: int = 0, selections: dict | None = None, model: str | None = None, session_id: str | None = None, request_id: str | None = None) -> dict | examples/python/co_consult/main.py | Build a full proposal; returns session_id for subsequent chat turns. |
plan_trip | traversal | (brief: dict, selections: dict | None = None, model: str | None = None, country: str = '', nights: int = 0) -> dict | examples/python/co_consult/main.py | Resolve the route, run the three supplier desks in parallel, add advisory context, propose. |
Commands
ATFGRAPH_SERVER=http://127.0.0.1:18880 python -m examples.python.co_consult.client_demo
docker compose -f examples/python/co_consult/compose.yaml up --build
python -m atfgraph.worker --module examples.python.co_consult.main:app --server http://127.0.0.1:18880
python -m atfgraph.worker --module node:app --server http://127.0.0.1:18880
Files
examples/python/co_consult/README.mdexamples/python/co_consult/__init__.pyexamples/python/co_consult/agent.pyexamples/python/co_consult/agentnode.yamlexamples/python/co_consult/client_demo.pyexamples/python/co_consult/conversation.pyexamples/python/co_consult/desks/__init__.pyexamples/python/co_consult/desks/activities.pyexamples/python/co_consult/desks/context.pyexamples/python/co_consult/desks/flights.pyexamples/python/co_consult/desks/hotels.pyexamples/python/co_consult/desks/intake.pyexamples/python/co_consult/desks/proposal.pyexamples/python/co_consult/fixtures.pyexamples/python/co_consult/helpers.pyexamples/python/co_consult/main.pyexamples/python/co_consult/models.pyexamples/python/co_consult/node.pyexamples/python/co_consult/requirements.txtexamples/python/co_consult/response_types.pyexamples/python/co_consult/runtime.pyexamples/python/co_consult/sample_chat.jsonexamples/python/co_consult/sample_payload.jsonexamples/python/co_consult/saved.pyexamples/python/co_consult/shopstore.pyexamples/python/co_consult/suppliers/__init__.pyexamples/python/co_consult/suppliers/config.pyexamples/python/co_consult/suppliers/http.pyexamples/python/co_consult/suppliers/livn.pyexamples/python/co_consult/suppliers/sabre.pyexamples/python/co_consult/suppliers/serpapi.py
README
Follow a travel request through intake, parallel flight/hotel/activity searches, advisory research and an evidence-checked proposal. Follow-up chat reuses saved searches. The example registers 31 capabilities under co-consult, version 0.3.0, and includes an installable node manifest with entrypoint node:app. This is application logic built on the shared runtime.
Choose supplier behavior explicitly with CO_CONSULT_MODE:
| Mode | Flights | Hotels and advisory context | Activities | Network |
|---|---|---|---|---|
fixture (explicit; tests and offline demos) | synthetic offers labelled DEMO DATA | synthetic properties | synthetic catalogue | none |
live (default) | Sabre Flight Shop (POST {SABRE_API_BASE}/v1/offers/flightShop) | SerpAPI google_hotels, google_flights, google, google_maps engines | LIVN Databricks vector search behind a managed MCP server (streamable HTTP, JSON-RPC 2.0) | bounded async HTTP |
Live failures remain visible: a desk reports missing settings, network failures or provider rejection, and the proposal lists the gap as partial. It never substitutes fixture inventory. This application does not book or hold inventory. Sabre quotes need revalidation, hotel rates are indicative, and activities show catalogue from-prices. Currency stays attached to each price; totals are not silently combined across currencies.
Decision generation has its own switch. The default CO_CONSULT_DECISIONS=model uses the configured SDK model URL/name. Select CO_CONSULT_DECISIONS=fixture for deterministic decisions; the proposal identifies them in confidence_caveats. Supplier fixture mode alone does not disable model calls.
flowchart TD
entry[consult or chat] --> intake[intake_brief -> extract_brief]
intake --> route[plan_trip -> resolve_route -> resolve_airport if needed]
route --> flights[flights_desk]
route --> hotels[hotels_desk]
route --> activities[activities_desk]
flights --> fare[flight_shop: fixture or Sabre -> rank_flight_offers -> interpret_fare_rules -> offer_detail from the saved shop]
hotels --> property[hotel_search: fixture or SerpAPI -> rank_hotel_shortlist -> assess_property -> hotel_detail, cached per token]
activities --> catalogue[frame_activity_query -> activity_search: fixture or LIVN MCP, up to twice -> rank_activities]
fare --> context[context_desk -> plan_context_lookups -> context_lookup: fixture or SerpAPI -> summarise_lookup]
property --> context
catalogue --> context
context --> propose[propose -> provenance_check + budget_fit -> compose_recommendation -> build_proposal]
propose --> saved[Session memory: brief, shops, tracks, proposal]
saved --> follow[chat follow-up or new selections; no supplier call]
Nested calls use app.traverse, giving each child its own ID under a shared root. Session memory stores raw supplier results under co-consult.shop.<shop_id>; child inputs pass compact shortlists rather than full responses.
Live supplier clients (suppliers/)
| Module | Endpoint semantics | Auth | Records saved under the shop |
|---|---|---|---|
sabre.py | POST /v1/offers/flightShop with journeys[{departureLocation.airportCode, arrivalLocation.airportCode, departureDate}], travelers[{passengerTypeCode}] (ADT/CNN/INF from the party), retailing.returnOfferAttributes: [Baggage, Flexibility]. Response offers/flights/journeys/offerAttributes are normalized to fare_offer, flight_segment, journey, fare_rule, baggage_allowance; the brief carries the five cheapest offers. offer_detail resolves refs from the saved shop, never a new search. | OAuth2 client credentials at SABRE_AUTH_URL with Basic base64(base64("V1:EPR:PCC:DOMAIN") ":" base64(secret)), cached until expires_in, re-minted once on HTTP 401; or a pre-minted SABRE_ACCESS_TOKEN | fs_<id> |
serpapi.py | Signed GET SERPAPI_URL?engine=google_hotels&q&check_in_date&check_out_date&adults&children&children_ages¤cy&gl&hl=en for the search and the same parameters plus property_token for detail (dates, party and currency must match the saved search). Advisory engines: google_flights (departure_id, arrival_id, outbound_date, return_date, adults, currency, type), google (q, location, num=8), google_maps (q, ll, type=search). Web results keep only attributable snippets with links. | api_key query parameter from SERPAPI_API_KEY | gh_<id> |
livn.py | MCP streamable HTTP: initialize (protocol 2025-06-18, clientInfo co-consult), notifications/initialized, then one tools/call of LIVN_MCP_TOOL with {"query": …}. Honours Mcp-Session-Id; accepts JSON or SSE-framed replies. Text content is a JSON array of catalogue rows (fromPrices, locationsEnd as JSON strings) normalized to activity_id, from_price, currency, duration_minutes, age_min, deduplicated on name. | Authorization: Bearer LIVN_DATABRICKS_TOKEN | lv_<id> |
Supplier clients share an 8-connection HTTP pool with a 16 MB response cap. Timeout defaults to 60 seconds; CO_CONSULT_SUPPLIER_RETRIES defaults to two extra attempts for transport errors and HTTP 408/429/5xx, with exponential backoff. Other 4xx responses do not retry.
Reported errors scrub configured secrets and Bearer/API-key fragments; raw exception text is excluded. Startup checks refer to variable names rather than values. In live mode, check_live_configuration() refuses startup when no supplier is configured and reports missing names otherwise.
Environment (names only; set values in your own environment or node config)
| Variable | Purpose |
|---|---|
CO_CONSULT_MODE | live (default) or fixture |
CO_CONSULT_DECISIONS, ATFGRAPH_MODEL_URL, ATFGRAPH_MODEL_NAME, ATFGRAPH_MODEL_API_KEY | optional model-backed decisions |
MARKET_CURRENCY, MARKET_COUNTRY | defaults when the brief states none (AUD, au) |
SABRE_API_BASE, SABRE_AUTH_URL, SABRE_AUTH_EPR, SABRE_AUTH_PCC, SABRE_AUTH_DOMAIN, SABRE_AUTH_SECRET | Sabre service and token credentials; SABRE_ACCESS_TOKEN alternatively |
SERPAPI_URL, SERPAPI_API_KEY | hotels and advisory lookups |
LIVN_MCP_URL, LIVN_MCP_TOOL, LIVN_DATABRICKS_TOKEN | activities MCP endpoint, exact tool name, bearer token |
CO_CONSULT_SUPPLIER_TIMEOUT, CO_CONSULT_SUPPLIER_RETRIES, CO_CONSULT_SUPPLIER_BACKOFF | live client bounds |
ATFGRAPH_MLFLOW_TRACING, MLFLOW_TRACKING_URI, ATFGRAPH_MLFLOW_EXPERIMENT | tracing, on by default (true, databricks, /Shared/atfgraph/co-consult) |
ATFGRAPH_MLFLOW_CAPTURE_PAYLOADS | true records each step's inputs and outputs and each model call's prompt and reply on the trace spans; off by default because the traces then hold the traveller's brief and supplier results |
ATFGRAPH_MODEL_PRICES | optional rate table (USD per million tokens, keyed by model name) so Databricks model calls carry an estimated cost instead of unpriced; see docs/python/06-model-calls.md |
Set values in your environment or managed node configuration. The example itself does not load .env; installed-node commands manage their own environment files and exclude them from package archives.
Tracing
main.py defaults to MLflow enabled, tracking URI databricks, and experiment /Shared/atfgraph/co-consult; existing environment values win. Requirements include MLflow. For successful export, the App service principal needs experiment permissions, or a laptop needs a Databricks profile/host credential. Set ATFGRAPH_MLFLOW_TRACING=false to disable it or choose a local tracking URI.
The Console links stored trace metadata when forwarding succeeds. Default traces include execution names, timing, outcomes and usage, excluding payloads. ATFGRAPH_MLFLOW_CAPTURE_PAYLOADS=true adds inputs, outputs, prompts and replies for evaluation; experiment readers can then see traveller and supplier content. See the MLflow guide for capture and redaction controls.
Run
From the vNext checkout (module form)
From a checkout with the SDK installed and a server listening on 127.0.0.1:18880:
python -m atfgraph.worker --module examples.python.co_consult.main:app --server http://127.0.0.1:18880
ATFGRAPH_SERVER=http://127.0.0.1:18880 python -m examples.python.co_consult.client_demo
For live supplier searches, change CO_CONSULT_MODE to live and configure the supplier variables. Configure decision mode separately.
As an installed agent node (directory packaging)
The package manifest uses node:app. Its node.py makes the project importable as co_consult, preserving relative imports when installed and run from the package root.
target/debug/atfg install examples/python/co_consult --python python3 --sdk python
target/debug/atfg run co-consult
target/debug/atfg traverse co-consult.consult --new-session \
--in '{"request":"A relaxed family trip with museums and gardens","origin":"Brisbane","destination":"Tokyo","depart_date":"2027-05-01","return_date":"2027-05-08","adults":2,"children_ages":"8,11","budget":"AUD 6000"}'
target/debug/atfg traverse co-consult.chat --session <session_id> --in '{"message":"Is that fare refundable?"}'
target/debug/atfg logs co-consult
target/debug/atfg stop co-consult
target/debug/atfg package examples/python/co_consult --out target/dist/packages
To run without package installation, start the worker from the project root:
cd examples/python/co_consult && python -m atfgraph.worker --module node:app --server http://127.0.0.1:18880
HTTP
curl -s -X POST http://127.0.0.1:18880/api/v2/sessions -H 'content-type: application/json' -d '{}'
curl -s -X POST http://127.0.0.1:18880/api/v2/traversers -H 'content-type: application/json' \
-d '{"target":"co-consult.chat","session_id":"<session_id>","input":{"message":"Plan a family trip","hints":{"origin":"Brisbane","destination":"Tokyo","depart_date":"2027-05-01","return_date":"2027-05-08","adults":2,"children_ages":[8,11],"budget":"AUD 6000"}}}'
curl -s http://127.0.0.1:18880/api/v2/traversers/<id>
curl -s http://127.0.0.1:18880/api/v2/traversers/<id>/path
Docker Compose
Choose either the repository's complete stack or a standalone worker scaffold:
Complete stack. deploy/docker/compose.yaml includes the server and co-consult worker, using examples.python.co_consult.main:app by default with fixture suppliers. The Compose smoke/journey scripts exercise this stack:
export ATFGRAPH_API_KEY="$(openssl rand -hex 24)" # required: the server binds 0.0.0.0 in its container
target/debug/atfg deploy compose up --name demo --build
target/debug/atfg deploy compose status --name demo
target/debug/atfg --server http://127.0.0.1:18880 --api-key "$ATFGRAPH_API_KEY" ls
target/debug/atfg deploy compose down --name demo --volumes --forget
For live suppliers, pass CO_CONSULT_MODE=live and supplier settings through the Compose environment. Review deploy/docker/README.md for image selection and rollback. Check decision settings separately before expecting a fully offline run.
Standalone worker. To connect co-consult containers to an existing server, generate a Dockerfile and compose.yaml in the project directory:
target/debug/atfg deploy scaffold examples/python/co_consult --target docker --label co-consult --module node:app
docker compose -f examples/python/co_consult/compose.yaml up --build
The scaffold runs python -m atfgraph.worker --module node:app --server "$ATFGRAPH_SERVER". Add the SDK to its requirements and configure the server URL/key. Selecting --target kubernetes generates a kustomize base under k8s/. Generated project files are distinct from the checked-in complete stack.
Sessions and follow-ups
consult and chat create a server session when none is supplied, and return its opaque ID. Send that ID on follow-up calls:
{"session_id": "<returned>", "message": "Is that fare refundable?"}
{"session_id": "<returned>", "message": "Choose the five-star hotel", "selections": {"property_tokens": ["<property_token>"]}}
{"session_id": "<returned>", "message": "Search fresh options", "refresh": true, "hints": {"depart_date": "2027-05-08", "return_date": "2027-05-15"}}
Session state lives at co-consult.state, updated with revision checks. Follow-ups and selection changes reuse saved shops without supplier calls and return source shop_ids. Use refresh=true when changing trip facts. A stable request_id deduplicates turns; interrupted attempts are reported rather than silently shopping again.
The worker credential owns sessions created here. A calling application must authenticate its users and authorize which session IDs each may reuse; the chat interface is not itself a user-authentication system.
Verify
cd python && .venv/bin/python -m pytest -c pyproject.toml ../tests/python_test_coconsult.py ../tests/test_coconsult_live.py -q
The fixture suite uses the in-memory graph double with a worker to check nested routing, gates, evidence, currencies, sessions, refresh and duplicate/interrupted turns. test_coconsult_live.py exercises the live-client code against local fake Sabre, SerpAPI and LIVN MCP services. It checks request shapes, saved follow-ups, passenger types, partial failures, retry/auth bounds, configuration errors, SSE replies and packaging. Despite its name, that suite does not contact real suppliers. Separate live searches require working credentials and provider access.