Single-call structured research pipeline: Streamlit + Google GenAI SDK + Pydantic v2, producing schema-validated technical reports through a deterministic JSON contract.
One inference request per execution. The model emits a fully structured JSON document, parsed natively into a Pydantic object before touching the UI.
User Query
│
▼
ResearchEngine (cached, one HTTP client per server session)
│
▼
Google GenAI SDK (response_schema = ResearchReport)
│
▼
Native Pydantic Parsing → ResearchResult (report + latency + sources)
│
▼
Streamlit Session State (single-path execution guard)
│
▼
State-Driven UI Rendering
- Exactly one network request per research execution — no prompt chaining, no JSON stitching.
- Schema-first validation: unparsable payloads terminate before UI rendering.
- Execution ownership lives in a single
research_activeguard, making Streamlit reruns deterministic and duplicate API calls impossible. - Status-code-first error taxonomy; failures render as categorized operational messages, never raw stack traces.
- Per-run telemetry (model, latency, sources) via
loguruserver-side and a metrics row client-side.
if st.session_state.research_active:
... # the only code path allowed to touch the networkButton Click → research_active=True → single execution path → validated response
→ research_active=False → st.rerun() → state-driven rendering
| Model | Fields |
|---|---|
ExecutiveSummary |
high_level_synthesis, performance_breakdown |
CodeBoilerplate |
language, snippet, explanation |
ProductionRisk |
title, description |
ResearchReport |
executive_summary, code_boilerplate, production_risks |
language is schema-enforced and drives syntax highlighting (alias-normalized, with a safe-lexer fallback). Native SDK parsing:
response_schema=ResearchReport
response_mime_type="application/json"st.session_state is treated as a backend data contract, not ad-hoc UI storage.
| Key | Type | Purpose |
|---|---|---|
research_active |
bool |
Owns the execution lifecycle; sole owner of network invocation |
raw_query |
str |
Canonical query submitted to the engine |
report_data |
dict | None |
Validated structured report |
error_message |
str | None |
Categorized user-facing error |
run_meta |
dict | None |
Telemetry: model ID, latency, sources |
query_input_widget |
str |
Text-area synchronization across reruns |
Rules: widgets never render from raw API responses; tabs consume persisted state only; the text area is the sole writer of raw_query; success and failure both reset the guard before rerun.
Classification priority: typed validation errors → HTTP status code (match on 429 / 401 / 403 / 5xx) → exception type → substring heuristics (documented last resort for non-SDK exceptions).
| Category | Signal |
|---|---|
| API Quota Exhaustion | HTTP 429 / "quota" |
| Authentication Error | HTTP 401/403 / key errors |
| Network Timeout / Server Unavailable | HTTP 5xx, TimeoutError, ConnectionError |
| Schema Validation Failure | Pydantic / parse errors |
| Unexpected System Error | Fallback catch-all |
On every failure: categorized message stored, stale report cleared, guard reset, consistent rerun.
22 deterministic tests, fully mocked at the SDK boundary — no network, no API keys, no quota consumption.
pip install -r requirements-dev.txt
pytest tests/ # 22 passedCoverage: schema contract (required-field enforcement, round-trip), language normalization & lexer fallback, engine lifecycle (secrets → env → default resolution), single-call request shape (exactly one generate_content, no tools), parse-failure path, and the full error taxonomy against typed ClientError/ServerError status codes.
Every push and PR to main runs .github/workflows/ci.yml (ubuntu-latest, Python 3.11): installs both requirement sets and executes the suite. The badge above reflects the latest run.
Modern Dark Minimalist (Linear/Vercel-grade), committed in .streamlit/config.toml plus a presentation-only CSS layer:
- Base
#0B0F19· surfaces#121620· text#FFFFFF - Electric cyan
#00E5FFreserved for primary action, key metrics, active tab - Glassmorphic cards:
rgba(255,255,255,0.03)+ 12px blur + 1px hairline borders, no heavy shadows
multi-ai-research-digest/
├── .github/workflows/ci.yml # CI: install + pytest on push/PR
├── .streamlit/config.toml # committed theme
├── tests/
│ ├── __init__.py
│ ├── test_error_handling.py # error taxonomy (typed status codes)
│ └── test_research_engine.py# schema, language, engine, telemetry
├── app.py # state-driven presentation layer
├── research_engine.py # single-call synthesis engine
├── requirements.txt
├── requirements-dev.txt
├── pytest.ini
├── .gitignore
└── README.md
git clone https://github.com/Ali-datasmith/multi-ai-research-digest.git
cd multi-ai-research-digest
pip install -r requirements.txt
export GOOGLE_API_KEY="your_api_key"
export GEMINI_MODEL="gemini-3.5-flash"
streamlit run app.pyStreamlit Community Cloud: credentials go in Advanced Settings → Secrets (never in the repo):
GOOGLE_API_KEY = "your_api_key_here"
GEMINI_MODEL = "gemini-3.5-flash"- Single inference request per execution
- Deterministic state transitions; explicit execution ownership
- Schema-first response validation
- UI rendered exclusively from persisted application state
- Status-code-first, human-readable error classification
- Cached engine: one HTTP client per server session
- Presentation-layer theming with zero impact on the execution path
- No retrieval grounding, by design. Google Search grounding was excluded because it draws from a separate, tightly capped free-tier quota pool; synthesis relies on the model's parametric knowledge. Treat output as an architectural starting point, not a cited source.
- Stateless by design. Each execution is an independent deterministic digest; multi-turn memory is intentionally out of scope.
- Residual heuristic fallback. Exceptions carrying no HTTP status code are classified by substring heuristics as a documented last resort.
