Developer GuideCurated questions (clients)

Curated questions integration (clients)

Generate Ask AI suggestion chips from data the client already fetched. The LLM does not run inside market-data use cases — call GenerateCuratedQuestionsUseCase in parallel after rendering so page load stays on the data path.


When to use this

Use caseUse curated questions?
Dashboard card (regime, macro, sectors, events, watchlist)Yes — pass the same JSON your UI already has
Options chain / quote / positioning panelYes — model_dump(mode="json") from domain models
Full question-driven analyzeNo — user already typed a question; use AnalyzeInstrumentRequest
Static onboarding chips onlyOptional — skip LLM when meta.llm_unavailable_reason is set

Container entry point

from copinance_os import (
    ArtifactType,
    CuratedQuestionsBlock,
    GenerateCuratedQuestionsRequest,
    FinancialLiteracy,
)
from copinance_os.ai.llm.config import LLMConfig
from copinance_os.infra.di import get_container
 
container = get_container(llm_config=LLMConfig(provider="openai", api_key="sk-..."))
uc = container.generate_curated_questions_use_case()

Request and response

block: CuratedQuestionsBlock = await uc.execute(
    GenerateCuratedQuestionsRequest(
        artifact=ArtifactType.QUOTE,
        payload={"symbol": "SPY", "current_price": 500, "volume": 1_000_000},
        count=5,
        financial_literacy=FinancialLiteracy.BEGINNER,
        learning_step=3,  # optional; dashboard STEP_PROMPTS alignment
        no_cache=False,
    ),
    llm_provider_override=user_provider,  # optional per-user API key
)
FieldNotes
artifactArtifactType — payload shape, not OS use-case name
payloadAlready-fetched dict; validated before LLM (see below)
count1–10, default 3
financial_literacySame tiers as analyze; see Financial literacy (clients)
learning_stepOptional 1–8 for tier-1 dashboard copy alignment
no_cacheBypass OS cache when debugging

CuratedQuestion (each chip)

FieldPurpose
textSuggested user question (max 500 chars)
focusOptional topic tag (volatility, liquidity, …)
suggested_toolsSoft hint: question-driven tool registry names (filtered server-side)
requires_symbolFor multi-symbol payloads (watchlist, sectors) — attach when sending to chat

meta (always present)

FieldPurpose
artifactEcho of request artifact
requested_countEcho of count
llm_enabledTrue only when LLM returned parsed questions
llm_unavailable_reasonTyped enum when questions is empty (see fallback)
generated_atUTC when LLM succeeded
cache_hitTrue when served from OS curated-questions cache
numeric_grounding_policyEcho of NUMERIC_GROUNDING_POLICY for client disclaimers

ArtifactType and payloads

ArtifactTypeTypical payload sourceValidation highlights
options_chainOptionsChain.model_dump(mode="json")Full chain model
quoteGetQuoteResponse.quote + symbolsymbol required
historical_bars{symbol, data: [...]} or barsNon-empty bar list
instrumentStock.model_dump(mode="json")Stock model
fundamentalsStockFundamentals.model_dump(mode="json")Fundamentals model
market_regimeYour market_context / regime card JSONFlexible dict
macro_snapshotDashboard macro stripindicators list (may be empty)
sector_rotationSector treemap APINon-empty sectors
upcoming_eventsEvents timeline APINon-empty events
watchlist_riskWatchlist risk summaryitems or symbols
options_positioningOptionsPositioningResult / positioning APIsymbol required

Invalid payloads raise domain ValidationError — no LLM call.


LLM unavailable fallback (BFF contract)

When the LLM cannot produce questions:

{
  "questions": [],
  "meta": {
    "llm_enabled": false,
    "llm_unavailable_reason": "provider_error"
  }
}
llm_unavailable_reasonSuggested client behavior
no_llm_configNo provider on container; show static chips only
provider_errorBad key, provider down, or failed health check — static chips
parse_errorModel returned non-JSON — static chips
rate_limitedBack off; do not hammer the endpoint
timeoutRetry later or static chips

Distinction: no_llm_config means no LLM was wired. provider_error means a provider was configured but failed (e.g. invalid API key).


Per-user API keys (llm_provider_override)

Apps that swap LLM credentials per user should not rebuild the whole container on every request:

block = await uc.execute(request, llm_provider_override=user_openai_provider)

The container default remains the fallback when override is omitted.


Caching

  • OS cache stores only curated-questions LLM output (namespace curated_questions), keyed by artifact + payload hash + literacy + count + learning step.
  • Dashboard artifacts (regime, macro, sectors, events, watchlist): longer TTL (~5 min).
  • Symbol snapshots (quote, chain, positioning): shorter TTL (~90 s).
  • BFFs may add their own cache (e.g. 60 s per user) on top.

Set no_cache=True when debugging prompt or payload changes.


Wiring chips into chat

  1. Fetch data → render UI immediately.
  2. Fire generate_curated_questions_use_case async.
  3. Show static chips first; append LLM chips when block arrives (dedupe by text).
  4. On chip click: pre-fill composer with text; optionally pass suggested_tools into question-driven analyze context (additive — not required by core today).

Questions are prompts for a later question-driven run — they must not assert numbers absent from the summarized DATA snapshot.