/\ ATFGraph /docs
README Downloads
Examples / ATFGraph
Framework reference

This article is retained from the existing framework docs. Self-hosting commands and server-admin access differ from the managed CLI workflow. Start with Get started for the installed product.

Co-consult

Directory examples/python/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.

Agent node label co-consult, version 0.3.0.

Capabilities

CapabilityKindSignatureModuleSummary
activities_desktraversal(destination: str, party: str, preferences: str = '', model: str | None = None) -> dictexamples/python/co_consult/desks/activities.pyThe activities track: frame the query, search the catalogue (twice at most), shortlist for this party.
activity_searchskill(query: str) -> dictexamples/python/co_consult/desks/activities.pySemantic search over the activities catalogue (fixture, or LIVN vector search in live mode). Returns {shop_id, activities, counts} or {error}.
frame_activity_querytraversal(destination: str, party: str, preferences: str, model: str | None = None) -> dictexamples/python/co_consult/desks/activities.pyGiven destination, party and interests, write a primary and an alternate semantic query.
rank_activitiestraversal(activities: list[dict], party: str, preferences: str, model: str | None = None) -> dictexamples/python/co_consult/desks/activities.pyGiven the deduplicated catalogue hits, shortlist up to six with fit notes.
context_desktraversal(situation: str, route: dict, dates: dict, adults: int, currency: str, model: str | None = None) -> dictexamples/python/co_consult/desks/context.pyThe advisory track: plan the lookups this proposal needs, run them in parallel, summarise each.
context_lookupskill(engine: str, query: dict) -> dictexamples/python/co_consult/desks/context.pyOne advisory lookup (fare_insights | destination_context | map_context | web_context | reviews_context); fixture or SerpAPI.
plan_context_lookupsdecision(situation: str, model: str | None = None) -> dictexamples/python/co_consult/desks/context.pyGiven what the suppliers returned, decide which advisory lookups to run and phrase each question.
summarise_lookuptraversal(engine: str, question: str, result: dict, model: str | None = None) -> dictexamples/python/co_consult/desks/context.pyGiven one raw lookup result, answer the question in 2-3 sentences with the source named.
flight_shopskill(legs: list[dict], travelers: list[str]) -> dictexamples/python/co_consult/desks/flights.pyRun a flight shop (fixture, or Sabre Flight Shop in live mode). Returns {shop_id, offers, counts} or {error, detail}.
flights_desktraversal(legs: list[dict], travelers: list[str], party: str, preferences: str = '', model: str | None = None) -> dictexamples/python/co_consult/desks/flights.pyThe flights track: shop, rank for this party, read the rules on the top picks.
interpret_fare_rulestraversal(shop_id: str, offer_id: str, party: str, model: str | None = None) -> dictexamples/python/co_consult/desks/flights.pyGiven a stored offer, return refundability, change policy and baggage in plain language.
offer_detailskill(shop_id: str, offer_id: str) -> dictexamples/python/co_consult/desks/flights.pyResolve one offer's itinerary, fare rules and baggage from a stored shop. Never re-searches.
rank_flight_offerstraversal(offers: list[dict], party: str, preferences: str, model: str | None = None) -> dictexamples/python/co_consult/desks/flights.pyGiven the shortlist and the party, pick the recommended offer and a runner-up with a reason.
assess_propertytraversal(property_token: str, search: dict, party: str, preferences: str, model: str | None = None) -> dictexamples/python/co_consult/desks/hotels.pyGiven one property's full detail, judge its fit for this party.
hotel_detailskill(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') -> dictexamples/python/co_consult/desks/hotels.pyResolve one property by its token. Dates, party and location must match the saved search.
hotel_searchskill(location: str, check_in_date: str, check_out_date: str, adults: int = 2, children_ages: str = '', currency: str = 'AUD', country: str = 'au') -> dictexamples/python/co_consult/desks/hotels.pySearch hotels for a destination and dates (fixture, or SerpAPI google_hotels in live mode). Returns {shop_id, properties, counts} or {error}.
hotels_desktraversal(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) -> dictexamples/python/co_consult/desks/hotels.pyThe hotels track: search, shortlist for this party, assess each pick in full.
rank_hotel_shortlisttraversal(properties: list[dict], party: str, budget_note: str, preferences: str, currency: str, model: str | None = None) -> dictexamples/python/co_consult/desks/hotels.pyGiven the search results and the party, pick up to three properties with a reason each.
extract_brieftraversal(request: str, today: str, model: str | None = None) -> dictexamples/python/co_consult/desks/intake.pyGiven the traveller's sentence, return the structured facts it contains.
intake_brieftraversal(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) -> dictexamples/python/co_consult/desks/intake.pyNormalize a request into a brief and report whether it is workable (status ready | needs_requirements).
resolve_airporttraversal(place: str, model: str | None = None) -> dictexamples/python/co_consult/desks/intake.pyGiven a city or region, return its main international airport IATA code (or unconfident).
resolve_routetraversal(brief: dict, model: str | None = None) -> dictexamples/python/co_consult/desks/intake.pyTurn a ready brief into flight legs (IATA, travel order) and passenger-type codes.
budget_fittraversal(budget_amount: float, currency: str, totals: dict, model: str | None = None) -> dictexamples/python/co_consult/desks/proposal.pyCompare like currencies in code; an incomplete subtotal is never a full-trip quote.
build_proposalskill(selection: dict, brief: dict, sections: dict, gaps: list[str], advisory: list[dict], budget: dict, provenance: dict) -> dictexamples/python/co_consult/desks/proposal.pyAssemble the reviewable proposal document from checked evidence.
compose_recommendationtraversal(material: str, model: str | None = None) -> dictexamples/python/co_consult/desks/proposal.pyWrite the proposal sections from supplied evidence only.
proposetraversal(brief: dict, route: dict, flights: dict, hotels: dict, activities: dict, context: dict, selections: dict | None = None, model: str | None = None) -> dictexamples/python/co_consult/desks/proposal.pySelect, provenance-check, budget-check, compose and build the proposal.
provenance_checkskill(selection: dict) -> dictexamples/python/co_consult/desks/proposal.pyCheck item membership AND quoted fields against this session's raw records.
answer_savedtraversal(message: str, evidence: dict, model: str | None = None) -> dictexamples/python/co_consult/main.pyNo supplier tools: answer only from saved, session-scoped evidence.
chattraversal(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) -> ChatResponseexamples/python/co_consult/main.pyKeep a brief and proposal across turns; only refresh=true repeats supplier shopping.
consulttraversal(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) -> dictexamples/python/co_consult/main.pyBuild a full proposal; returns session_id for subsequent chat turns.
plan_triptraversal(brief: dict, selections: dict | None = None, model: str | None = None, country: str = '', nights: int = 0) -> dictexamples/python/co_consult/main.pyResolve the route, run the three supplier desks in parallel, add advisory context, propose.

Commands

Taken from this example's own README and module docstrings.

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.md
  • examples/python/co_consult/__init__.py
  • examples/python/co_consult/agent.py
  • examples/python/co_consult/agentnode.yaml
  • examples/python/co_consult/client_demo.py
  • examples/python/co_consult/conversation.py
  • examples/python/co_consult/desks/__init__.py
  • examples/python/co_consult/desks/activities.py
  • examples/python/co_consult/desks/context.py
  • examples/python/co_consult/desks/flights.py
  • examples/python/co_consult/desks/hotels.py
  • examples/python/co_consult/desks/intake.py
  • examples/python/co_consult/desks/proposal.py
  • examples/python/co_consult/fixtures.py
  • examples/python/co_consult/helpers.py
  • examples/python/co_consult/main.py
  • examples/python/co_consult/models.py
  • examples/python/co_consult/node.py
  • examples/python/co_consult/requirements.txt
  • examples/python/co_consult/response_types.py
  • examples/python/co_consult/runtime.py
  • examples/python/co_consult/sample_chat.json
  • examples/python/co_consult/sample_payload.json
  • examples/python/co_consult/saved.py
  • examples/python/co_consult/shopstore.py
  • examples/python/co_consult/suppliers/__init__.py
  • examples/python/co_consult/suppliers/config.py
  • examples/python/co_consult/suppliers/http.py
  • examples/python/co_consult/suppliers/livn.py
  • examples/python/co_consult/suppliers/sabre.py
  • examples/python/co_consult/suppliers/serpapi.py

README

Rendered from examples/python/co_consult/README.md; the source file is not modified.

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:

ModeFlightsHotels and advisory contextActivitiesNetwork
fixture (explicit; tests and offline demos)synthetic offers labelled DEMO DATAsynthetic propertiessynthetic cataloguenone
live (default)Sabre Flight Shop (POST {SABRE_API_BASE}/v1/offers/flightShop)SerpAPI google_hotels, google_flights, google, google_maps enginesLIVN 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/)

ModuleEndpoint semanticsAuthRecords saved under the shop
sabre.pyPOST /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_TOKENfs_<id>
serpapi.pySigned GET SERPAPI_URL?engine=google_hotels&q&check_in_date&check_out_date&adults&children&children_ages&currency&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_KEYgh_<id>
livn.pyMCP 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_TOKENlv_<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)

VariablePurpose
CO_CONSULT_MODElive (default) or fixture
CO_CONSULT_DECISIONS, ATFGRAPH_MODEL_URL, ATFGRAPH_MODEL_NAME, ATFGRAPH_MODEL_API_KEYoptional model-backed decisions
MARKET_CURRENCY, MARKET_COUNTRYdefaults when the brief states none (AUD, au)
SABRE_API_BASE, SABRE_AUTH_URL, SABRE_AUTH_EPR, SABRE_AUTH_PCC, SABRE_AUTH_DOMAIN, SABRE_AUTH_SECRETSabre service and token credentials; SABRE_ACCESS_TOKEN alternatively
SERPAPI_URL, SERPAPI_API_KEYhotels and advisory lookups
LIVN_MCP_URL, LIVN_MCP_TOOL, LIVN_DATABRICKS_TOKENactivities MCP endpoint, exact tool name, bearer token
CO_CONSULT_SUPPLIER_TIMEOUT, CO_CONSULT_SUPPLIER_RETRIES, CO_CONSULT_SUPPLIER_BACKOFFlive client bounds
ATFGRAPH_MLFLOW_TRACING, MLFLOW_TRACKING_URI, ATFGRAPH_MLFLOW_EXPERIMENTtracing, on by default (true, databricks, /Shared/atfgraph/co-consult)
ATFGRAPH_MLFLOW_CAPTURE_PAYLOADStrue 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_PRICESoptional 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.

ATFGraph documentationHosted workflow · Framework reference