Skip to content

Commit 87e6aba

Browse files
authored
Merge pull request #232 from Open-Finance-Lab/fingpt_backend_prod
prompt: enhancing system prompt
2 parents 740eaf6 + c18a9b4 commit 87e6aba

2 files changed

Lines changed: 142 additions & 1 deletion

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Streaming-by-Phase Research Engine
2+
3+
## Overview
4+
5+
Converted the blocking `run_iterative_research()` call into an async generator (`run_iterative_research_streaming()`) that yields phase-by-phase status updates via SSE. Users now see real-time feedback during the 2-3 minute research execution instead of a blank loading state.
6+
7+
**Branch**: `fingpt_backend_dev`
8+
**Date**: February 2026
9+
10+
---
11+
12+
## Architecture
13+
14+
### Streaming Protocol
15+
16+
The research engine communicates via a sentinel tuple convention through the existing `(text_chunk, entries)` yield protocol:
17+
18+
| Tuple Shape | Meaning |
19+
|---|---|
20+
| `(None, {"label": "...", "detail": "..."})` | Status event (phase transition) |
21+
| `("text", [])` | Synthesis content token |
22+
| `("", [source_dicts...])` | Source delivery |
23+
24+
The SSE view detects `text_chunk is None` and emits a status frame instead of a content frame. Non-research streams are unaffected.
25+
26+
### Generator Chain
27+
28+
```
29+
research_engine.run_iterative_research_streaming()
30+
-> datascraper._research_stream() (wraps + fallback logic)
31+
-> views.py event_stream() (SSE serialization)
32+
-> frontend onStatus callback (UI status card)
33+
```
34+
35+
### Phase Labels
36+
37+
| Phase | Label | Detail |
38+
|-------|-------|--------|
39+
| Query analysis | "Analyzing query" | *(none)* |
40+
| Plan formed | "Planning research" | "Identified N sub-questions" |
41+
| Sub-question completion | "Researching" | truncated sub-question (80 chars + "...") |
42+
| Gap detection | "Evaluating results" | "Checking completeness" |
43+
| Follow-up research | "Follow-up research" | truncated follow-up (80 chars + "...") |
44+
| Synthesis | "Synthesizing findings" | "Combining N results" |
45+
46+
---
47+
48+
## Files Modified
49+
50+
### `datascraper/research_engine.py`
51+
- **`_call_synthesis_streaming()`** — New helper for streaming synthesis calls with temperature retry
52+
- **`_call_planner()`** — Added temperature retry (gpt-5-mini rejects `temperature=0.0`)
53+
- **`_call_synthesis()`** — Added temperature retry (gpt-5.2-chat-latest rejects `temperature=0.2`)
54+
- **`Synthesizer._build_synthesis_messages()`** — DRY helper for message construction
55+
- **`Synthesizer.synthesize_streaming()`** — Async generator yielding tokens with non-streaming fallback; explicit `stream.close()` in `finally` block
56+
- **`_status()`** — Helper building `(None, {"label", "detail"})` sentinel tuples
57+
- **`run_iterative_research_streaming()`** — Main async generator orchestrating all phases:
58+
- Parallel sub-question execution via `asyncio.wait(FIRST_COMPLETED)` with per-completion status
59+
- Source deduplication and delivery before synthesis
60+
- Token-by-token synthesis streaming
61+
- Simple query bypass (yields nothing, triggers fallback)
62+
63+
### `datascraper/datascraper.py`
64+
- **`_research_stream()`** (lines ~840-919) — Async generator wrapping the research engine:
65+
- Passes through status events and sources
66+
- Tracks `content_started` flag (not `got_any`) to correctly distinguish status-only vs synthesis-producing runs
67+
- Falls through to single-search path (`_create_advanced_response_stream_async()`) if no synthesis content produced
68+
- If exception after content started, re-raises; otherwise falls through gracefully
69+
- **`get_sources()`** (line 1248) — Fixed `_get_or_create_session` -> `_load_session` (method didn't exist on `UnifiedContextManager`)
70+
71+
### `api/views.py`
72+
- **SSE loop** (lines ~586-620):
73+
- Added status event detection: `text_chunk is None and isinstance(entries, dict) and "label" in entries`
74+
- Added `isinstance(entries, list)` guard on source entries branch
75+
- Added `try/finally` with `stream_iter.aclose()` + `loop.shutdown_asyncgens()` for proper async generator cleanup
76+
77+
### `datascraper/models_config.py`
78+
- **`validate_model_support()`** — Added reverse lookup by `model_name` field so resolved names like `"gpt-5.2-chat-latest"` are recognized (not just display names like `"FinGPT"`)
79+
80+
### `frontend/src/modules/handlers.js`
81+
- Added 6 research phase labels to `STATUS_LABEL_REMAPPINGS` for user-friendly display
82+
83+
### `gunicorn.conf.py` + `Dockerfile`
84+
- Timeout extended to 1200s (20 minutes) to accommodate deep research runs with 3 iterations
85+
86+
### `tests/test_research_engine.py`
87+
- `test_streaming_simple_query_yields_nothing` — Bypass signal for simple queries
88+
- `test_streaming_status_event_format` — Label/detail type validation
89+
- `test_streaming_phases_in_order` — Phase ordering with parallel execution
90+
- `test_streaming_sources_before_synthesis` — Sources delivered before synthesis text
91+
92+
---
93+
94+
## Bugs Encountered & Fixes
95+
96+
### 1. Temperature rejection by OpenAI models
97+
**Symptom**: `400 Bad Request` on `_call_planner` (gpt-5-mini, temp=0.0) and `_call_synthesis`/`_call_synthesis_streaming` (gpt-5.2-chat-latest, temp=0.2).
98+
**Fix**: Try/except that retries without the `temperature` parameter when error message contains "temperature". Logged at INFO level.
99+
100+
### 2. Fallthrough logic: `got_any` vs `content_started`
101+
**Symptom**: Status events set `got_any=True`, preventing fallback to single-search when research engine produced no synthesis.
102+
**Fix**: Replaced with `content_started` flag that only triggers on actual synthesis text chunks.
103+
104+
### 3. Gunicorn worker timeout
105+
**Symptom**: Workers killed at 120s during deep research (5 sub-questions + 3 follow-ups per iteration x 3 iterations).
106+
**Root cause**: Dockerfile CMD hardcoded `--timeout 120`, overriding `gunicorn.conf.py`.
107+
**Fix**: Changed to `--timeout 1200` in both Dockerfile and gunicorn.conf.py.
108+
109+
### 4. `validate_model_support` failing for resolved model names
110+
**Symptom**: `gemini-3-flash-preview` flagged as "MCP not supported" even though `FinGPT` config has `supports_mcp: True`.
111+
**Fix**: Added reverse lookup by `model_name` field in `validate_model_support()`.
112+
113+
### 5. Source URL retrieval broken
114+
**Symptom**: `'UnifiedContextManager' object has no attribute '_get_or_create_session'` on `/get_source_urls/` endpoint.
115+
**Fix**: Changed to `_load_session()` which is the correct method (has get-or-create semantics internally).
116+
117+
### 6. Async generator cleanup warning
118+
**Symptom**: `Task was destroyed but it is pending! coro=<async_generator_athrow>` on every research completion.
119+
**Fix**: Added `loop.shutdown_asyncgens()` before `loop.close()` in views.py — this is what `asyncio.run()` does internally. Also added explicit `stream.close()` in `synthesize_streaming()` finally block.
120+
121+
---
122+
123+
## Test Results
124+
125+
All 21 tests pass:
126+
- 13 existing research engine tests
127+
- 4 new streaming tests
128+
- 4 research config tests
129+
130+
---
131+
132+
## Performance
133+
134+
| Metric | Before | After |
135+
|--------|--------|-------|
136+
| User feedback during research | None (blank loading) | Phase-by-phase status updates |
137+
| Sub-question execution | Sequential | Parallel (`asyncio.wait(FIRST_COMPLETED)`) |
138+
| Typical research duration | 60-120s | 60-120s (same, but with visibility) |
139+
| Sources returned | 10 (on synthesis failure) | 40-60+ (full research) |
140+
| Max supported duration | 120s (worker timeout) | 1200s (20 min) |

Main/backend/prompts/core.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ GENERAL RULES:
88
- Use $ for inline math and $$ for display equations.
99

1010
DATA ACCURACY:
11-
- For any numerical financial data, use values exactly as returned by tools. Never round, approximate, or re-derive a number when the exact figure is available from the data source.
11+
- For numerical financial data returned by tools (e.g., Yahoo Finance), present numbers rounded to 2 decimal places for readability (e.g., 234.5678901234 → 234.57, 0.0456789 → 0.05). If the user explicitly asks for exact or precise figures, provide the full unrounded value from the data source.
12+
- Never re-derive or fabricate a number when a value is available from the data source.
1213
- When a user specifies a particular data field (e.g., "Basic Shares Outstanding"), always use that specific reported value from the data source — never compute your own estimate (e.g., do NOT derive shares outstanding from market cap / price).
1314
- For percentage change: use the regularMarketChangePercent field if available, or compute from exact closing prices: (latest_close - previous_close) / previous_close * 100.
1415
- For turnover ratio: use the reported Shares Outstanding value from the stock's key statistics, not a self-computed estimate.

0 commit comments

Comments
 (0)