jeanbaptdzd commited on
Commit
3e6b9d2
·
1 Parent(s): 30adc14

chore: Clean up repo - remove redundant tests and docs, update README

Browse files

- Remove old test files from root (moved to tests/)
- Remove redundant documentation files
- Update README with new features (rate limiting, stats, PydanticAI)
- Keep HF header clean
- Add PydanticAI integration to project structure

.rebuild_trigger DELETED
@@ -1 +0,0 @@
1
- Mon Nov 17 16:59:47 CET 2025
 
 
CHANGES_SUMMARY.md DELETED
@@ -1,312 +0,0 @@
1
- # Changes Summary - Critical Issues Fixed
2
-
3
- ## Overview
4
- This document summarizes all the critical fixes and improvements implemented based on the code review.
5
-
6
- ---
7
-
8
- ## ✅ Critical Issues Fixed
9
-
10
- ### 1. Model Readiness Check in Health Endpoint
11
- **File:** `app/main.py`
12
-
13
- **Before:**
14
- ```python
15
- @app.get("/health")
16
- async def health() -> Dict[str, str]:
17
- return {"status": "healthy", "service": "LLM Pro Finance API"}
18
- ```
19
-
20
- **After:**
21
- ```python
22
- @app.get("/health")
23
- async def health() -> Dict[str, Any]:
24
- model_ready = _initialized and model is not None
25
- return {
26
- "status": "healthy" if model_ready else "initializing",
27
- "service": "LLM Pro Finance API",
28
- "model_ready": model_ready,
29
- }
30
- ```
31
-
32
- **Impact:** Health endpoint now accurately reports whether the model is ready to serve requests.
33
-
34
- ---
35
-
36
- ### 2. Error Message Sanitization
37
- **Files:** `app/routers/openai_api.py`
38
-
39
- **Changes:**
40
- - Separated `ValueError` (validation errors) from generic exceptions
41
- - Sanitized internal error messages to prevent information leakage
42
- - Added specific error handling for model reload endpoint
43
-
44
- **Before:**
45
- ```python
46
- except Exception as e:
47
- return JSONResponse(
48
- status_code=500,
49
- content={"error": {"message": str(e), "type": "internal_error"}}
50
- )
51
- ```
52
-
53
- **After:**
54
- ```python
55
- except ValueError as e:
56
- # Validation errors - safe to expose
57
- return JSONResponse(
58
- status_code=400,
59
- content={"error": {"message": str(e), "type": "invalid_request_error"}}
60
- )
61
- except Exception as e:
62
- # Internal errors - sanitize message
63
- logger.error(f"Error: {str(e)}", exc_info=True)
64
- return JSONResponse(
65
- status_code=500,
66
- content={"error": {"message": "An internal error occurred. Please try again later.", "type": "internal_error"}}
67
- )
68
- ```
69
-
70
- **Impact:** Prevents sensitive information from being exposed to clients.
71
-
72
- ---
73
-
74
- ### 3. Magic Numbers Extracted to Constants
75
- **File:** `app/utils/constants.py`
76
-
77
- **Added:**
78
- ```python
79
- # Model initialization constants
80
- MODEL_INIT_TIMEOUT_SECONDS = 300 # 5 minutes
81
- MODEL_INIT_WAIT_INTERVAL_SECONDS = 1
82
-
83
- # Rate limiting constants
84
- RATE_LIMIT_REQUESTS_PER_MINUTE = 30
85
- RATE_LIMIT_REQUESTS_PER_HOUR = 500
86
-
87
- # Confidence calculation constants
88
- MIN_ANSWER_LENGTH_FOR_HIGH_CONFIDENCE = 50
89
- ```
90
-
91
- **Updated:** `app/providers/transformers_provider.py` to use these constants instead of hardcoded values.
92
-
93
- **Impact:** Better maintainability and easier configuration.
94
-
95
- ---
96
-
97
- ### 4. Fixed Duplicate Regex
98
- **File:** `open-finance-pydanticAI/app/utils.py`
99
-
100
- **Before:** Duplicate regex pattern applied twice unnecessarily.
101
-
102
- **After:** Removed duplicate, keeping only one application.
103
-
104
- **Impact:** Cleaner code, slight performance improvement.
105
-
106
- ---
107
-
108
- ## 🆕 New Features
109
-
110
- ### 5. Rate Limiting
111
- **Files:**
112
- - `app/middleware/rate_limit.py` (new)
113
- - `app/middleware/__init__.py` (new)
114
- - `app/main.py` (updated)
115
-
116
- **Features:**
117
- - Simple in-memory rate limiter (suitable for demo/single user)
118
- - Per-minute limit: 30 requests
119
- - Per-hour limit: 500 requests
120
- - Rate limit headers in responses:
121
- - `X-RateLimit-Limit-Minute`
122
- - `X-RateLimit-Limit-Hour`
123
- - `X-RateLimit-Remaining-Minute`
124
- - `X-RateLimit-Remaining-Hour`
125
- - Automatic cleanup of old entries to prevent memory growth
126
- - Returns 429 status with `Retry-After` header when limit exceeded
127
-
128
- **Usage:** Automatically applied to all API endpoints except public ones (`/`, `/health`, `/docs`, `/v1/stats`).
129
-
130
- ---
131
-
132
- ### 6. Token Statistics Tracking
133
- **Files:**
134
- - `app/utils/stats.py` (new)
135
- - `app/providers/transformers_provider.py` (updated)
136
- - `app/main.py` (updated)
137
-
138
- **Features:**
139
- - Thread-safe statistics tracking
140
- - Tracks per-request:
141
- - Prompt tokens
142
- - Completion tokens
143
- - Total tokens
144
- - Model used
145
- - Finish reason
146
- - Timestamp
147
-
148
- **Aggregate Statistics:**
149
- - Total requests
150
- - Total tokens (prompt, completion, total)
151
- - Average tokens per request
152
- - Requests per hour
153
- - Tokens per hour
154
- - Requests by model
155
- - Tokens by model
156
- - Finish reason distribution
157
- - Uptime tracking
158
-
159
- **New Endpoint:** `GET /v1/stats`
160
- Returns comprehensive usage statistics and token counts.
161
-
162
- **Example Response:**
163
- ```json
164
- {
165
- "uptime_seconds": 3600,
166
- "uptime_hours": 1.0,
167
- "total_requests": 50,
168
- "total_prompt_tokens": 5000,
169
- "total_completion_tokens": 15000,
170
- "total_tokens": 20000,
171
- "average_prompt_tokens": 100.0,
172
- "average_completion_tokens": 300.0,
173
- "average_total_tokens": 400.0,
174
- "requests_per_hour": 50.0,
175
- "tokens_per_hour": 20000.0,
176
- "requests_by_model": {
177
- "DragonLLM/qwen3-8b-fin-v1.0": 50
178
- },
179
- "tokens_by_model": {
180
- "DragonLLM/qwen3-8b-fin-v1.0": 20000
181
- },
182
- "finish_reasons": {
183
- "stop": 45,
184
- "length": 5
185
- },
186
- "recent_requests_count": 50
187
- }
188
- ```
189
-
190
- ---
191
-
192
- ### 7. Improved Token Counting Accuracy
193
- **File:** `app/providers/transformers_provider.py`
194
-
195
- **Changes:**
196
- - Non-streaming: Uses `len(inputs.input_ids[0])` for prompt tokens (more accurate)
197
- - Streaming: Uses tokenizer to count tokens from generated text after streaming completes
198
-
199
- **Before:**
200
- ```python
201
- prompt_tokens = inputs.input_ids.shape[1] # Less accurate
202
- completion_tokens = len(generated_ids) # OK but could be better
203
- ```
204
-
205
- **After:**
206
- ```python
207
- prompt_tokens = len(inputs.input_ids[0]) # More accurate
208
- # For streaming:
209
- completion_tokens = len(tokenizer.encode(generated_text, add_special_tokens=False))
210
- ```
211
-
212
- **Impact:** More accurate token counting for billing/statistics.
213
-
214
- ---
215
-
216
- ## 📊 Statistics Tracking
217
-
218
- ### What's Tracked
219
- - Every chat completion request (streaming and non-streaming)
220
- - Token usage per request
221
- - Model usage patterns
222
- - Finish reasons (stop vs length)
223
- - Request rates
224
-
225
- ### Statistics Endpoint
226
- - **URL:** `GET /v1/stats`
227
- - **Access:** Public (no authentication required)
228
- - **Rate Limited:** No (excluded from rate limiting)
229
-
230
- ---
231
-
232
- ## 🔒 Security Improvements
233
-
234
- 1. **Error Message Sanitization:** Internal errors no longer expose sensitive details
235
- 2. **Rate Limiting:** Prevents abuse and resource exhaustion
236
- 3. **Input Validation:** Better separation of validation vs internal errors
237
-
238
- ---
239
-
240
- ## 📝 Files Modified
241
-
242
- ### New Files
243
- - `app/middleware/rate_limit.py` - Rate limiting middleware
244
- - `app/middleware/__init__.py` - Middleware package init
245
- - `app/utils/stats.py` - Statistics tracking module
246
- - `CHANGES_SUMMARY.md` - This file
247
-
248
- ### Modified Files
249
- - `app/main.py` - Health check, stats endpoint, middleware setup
250
- - `app/routers/openai_api.py` - Error sanitization
251
- - `app/providers/transformers_provider.py` - Token counting, stats tracking, constants
252
- - `app/utils/constants.py` - Added new constants
253
- - `app/middleware.py` - Added `/v1/stats` to public paths
254
- - `open-finance-pydanticAI/app/utils.py` - Fixed duplicate regex
255
-
256
- ---
257
-
258
- ## 🧪 Testing Recommendations
259
-
260
- 1. **Health Endpoint:**
261
- - Test when model is loading
262
- - Test when model is ready
263
- - Verify `model_ready` field
264
-
265
- 2. **Rate Limiting:**
266
- - Send 31 requests in 1 minute (should get 429 on 31st)
267
- - Verify rate limit headers
268
- - Test different IP addresses
269
-
270
- 3. **Statistics:**
271
- - Make several requests
272
- - Check `/v1/stats` endpoint
273
- - Verify token counts match request usage
274
-
275
- 4. **Error Handling:**
276
- - Test with invalid inputs (should get sanitized errors)
277
- - Test internal errors (should not expose details)
278
-
279
- ---
280
-
281
- ## 🚀 Deployment Notes
282
-
283
- 1. **Rate Limiting:** Currently in-memory, resets on server restart. For production with multiple servers, consider Redis-based rate limiting.
284
-
285
- 2. **Statistics:** Currently in-memory, resets on server restart. For production, consider persisting to database.
286
-
287
- 3. **Constants:** All rate limits and timeouts are configurable via `constants.py`.
288
-
289
- ---
290
-
291
- ## 📈 Performance Impact
292
-
293
- - **Rate Limiting:** Minimal overhead (~1ms per request)
294
- - **Statistics Tracking:** Minimal overhead (~0.5ms per request)
295
- - **Token Counting:** Slightly more accurate, negligible performance impact
296
-
297
- ---
298
-
299
- ## ✅ All Critical Issues Resolved
300
-
301
- - ✅ Model readiness check in health endpoint
302
- - ✅ Error message sanitization
303
- - ✅ Magic numbers extracted to constants
304
- - ✅ Duplicate regex fixed
305
- - ✅ Rate limiting added
306
- - ✅ Token statistics tracking added
307
- - ✅ Improved token counting accuracy
308
-
309
- ---
310
-
311
- **Status:** All critical issues from code review have been addressed. The codebase is now more secure, maintainable, and provides better observability.
312
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
CLEANUP_PLAN.md DELETED
@@ -1,155 +0,0 @@
1
- # Code Cleanup Plan
2
-
3
- ## Overview
4
- This document outlines the cleanup strategy for the simple-llm-pro-finance project to remove obsolete files and improve code organization.
5
-
6
- ## Files to Remove
7
-
8
- ### 1. Obsolete Test Scripts (Root Directory)
9
- **Reason:** All functional tests have been moved to `tests/` directory. These are one-off debugging scripts.
10
-
11
- - `analyze_performance.py` - Performance analysis done, results in FINAL_TEST_REPORT.md
12
- - `debug_chat_template.py` - Debug script, no longer needed
13
- - `final_clean_test.py` - One-off test
14
- - `investigate_french_consistency.py` - Investigation complete
15
- - `quiz_finance_francais.py` - Test script (also in git staging)
16
- - `test_advanced_finance.py` - Moved to tests/
17
- - `test_all_fixes.py` - One-off validation
18
- - `test_debug_endpoint.sh` - Shell test script
19
- - `test_finance_final.py` - One-off test
20
- - `test_finance_improved.py` - One-off test
21
- - `test_finance_queries.py` - One-off test
22
- - `test_french_direct.py` - One-off test
23
- - `test_french_final_check.py` - One-off test
24
- - `test_french_simple.sh` - Shell test script
25
- - `test_french_strategies.py` - One-off test
26
- - `test_generation_fix.sh` - Shell test script
27
- - `test_memory_stress.py` - Moved to tests/
28
- - `test_quick_french.py` - One-off test
29
- - `test_service.py` - One-off test
30
- - `test_system_prompt.py` - One-off test
31
- - `test_tokenizer_debug.py` - Debug script
32
- - `test_truncation_issue.py` - One-off test
33
-
34
- **Total:** 21 test files
35
-
36
- ### 2. Obsolete Documentation Files
37
- **Reason:** Superseded by comprehensive final reports.
38
-
39
- - `STATUS.md` - Historical status, superseded by FINAL_STATUS.md
40
- - `FIXES_SUMMARY.md` - Historical, covered in FINAL_TEST_REPORT.md
41
- - `PERFORMANCE_REPORT.md` - Covered in FINAL_TEST_REPORT.md
42
- - `memory_test_results.txt` - Old test results
43
- - `test_results.txt` - Old test results
44
-
45
- **Total:** 5 documentation files
46
-
47
- ### 3. Empty/Debug Code Directories
48
- **Reason:** Unused or debug-only code.
49
-
50
- - `app/utils/` - Empty directory (only __pycache__)
51
- - `app/routers/debug.py` - Debug endpoint not needed in production
52
-
53
- **Total:** 1 directory, 1 file
54
-
55
- ## Files to Keep
56
-
57
- ### Core Application
58
- - `app/` directory (except items listed for removal)
59
- - `main.py` - FastAPI application
60
- - `config.py` - Configuration
61
- - `middleware.py` - API key authentication
62
- - `models/openai.py` - Pydantic models
63
- - `providers/base.py` - Provider protocol
64
- - `providers/transformers_provider.py` - Main inference engine
65
- - `routers/openai_api.py` - OpenAI-compatible API
66
- - `services/chat_service.py` - Chat service wrapper
67
-
68
- ### Tests
69
- - `tests/` directory - Proper pytest structure
70
- - `conftest.py`
71
- - `test_config.py`
72
- - `test_middleware.py`
73
- - `test_openai_models.py`
74
- - `test_openai_routes.py`
75
- - `test_providers.py`
76
- - `performance/` - Performance benchmarks
77
-
78
- ### Documentation
79
- - `README.md` - Main documentation (needs cleanup)
80
- - `FINAL_STATUS.md` - Final deployment status
81
- - `FINAL_TEST_REPORT.md` - Comprehensive test results
82
- - `LICENSE` - MIT license
83
-
84
- ### Configuration & Deployment
85
- - `Dockerfile` - Docker build configuration
86
- - `requirements.txt` - Production dependencies
87
- - `requirements-dev.txt` - Development dependencies
88
-
89
- ### Scripts
90
- - `scripts/validate_hf_readme.py` - Useful validation utility
91
- - `scripts/README.md` - Scripts documentation
92
-
93
- ## Refactoring Needed
94
-
95
- ### 1. Remove Debug Router from Production
96
- **File:** `app/main.py`
97
- **Change:** Remove debug router import and mount
98
- ```python
99
- # Remove this line
100
- app.include_router(debug.router, prefix="/v1")
101
- ```
102
-
103
- ### 2. Clean Up README.md
104
- **File:** `README.md`
105
- **Changes:**
106
- - Remove outdated test coverage stats (91% reference)
107
- - Update to reflect current stable state
108
- - Simplify configuration section
109
- - Remove references to obsolete features
110
-
111
- ### 3. Remove Empty Utils Directory
112
- **Directory:** `app/utils/`
113
- **Action:** Delete the entire directory as it's unused
114
-
115
- ## Impact Assessment
116
-
117
- ### Breaking Changes
118
- **None** - All removed files are development/debugging artifacts.
119
-
120
- ### Non-Breaking Changes
121
- - Removing debug endpoint (`/v1/debug/prompt`) - Not documented in README
122
- - Cleaner project structure
123
- - Reduced repository size
124
-
125
- ### Benefits
126
- - **Clarity:** Easier to understand project structure
127
- - **Maintenance:** Fewer files to maintain
128
- - **Size:** Reduced repo size
129
- - **Professionalism:** Clean, production-ready codebase
130
-
131
- ## Execution Plan
132
-
133
- 1. ✅ Create backup branch
134
- 2. ✅ Remove obsolete test files
135
- 3. ✅ Remove obsolete documentation
136
- 4. ✅ Remove debug code
137
- 5. ✅ Update README.md
138
- 6. ✅ Run tests to verify nothing broke
139
- 7. ✅ Commit and push changes
140
-
141
- ## Success Criteria
142
-
143
- - ✅ All tests in `tests/` directory still pass
144
- - ✅ Application still starts and serves requests
145
- - ✅ README.md is accurate and up-to-date
146
- - ✅ No broken imports or references
147
- - ✅ Git history preserved (files deleted, not rewritten)
148
-
149
- ## Rollback Plan
150
-
151
- If issues arise:
152
- 1. Git checkout the cleanup branch: `git checkout pre-cleanup-backup`
153
- 2. Review what was removed
154
- 3. Restore only necessary files
155
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
CLEANUP_SUMMARY.md DELETED
@@ -1,190 +0,0 @@
1
- # Cleanup Summary - November 2, 2025
2
-
3
- ## Overview
4
- Comprehensive codebase cleanup to remove obsolete test scripts, redundant documentation, and debug code from the project.
5
-
6
- ## Files Removed
7
-
8
- ### Test Scripts (21 files)
9
- All one-off debugging and validation scripts have been removed. Proper tests remain in `tests/` directory.
10
-
11
- ✅ Removed:
12
- - `analyze_performance.py`
13
- - `debug_chat_template.py`
14
- - `final_clean_test.py`
15
- - `investigate_french_consistency.py`
16
- - `quiz_finance_francais.py`
17
- - `test_advanced_finance.py`
18
- - `test_all_fixes.py`
19
- - `test_debug_endpoint.sh`
20
- - `test_finance_final.py`
21
- - `test_finance_improved.py`
22
- - `test_finance_queries.py`
23
- - `test_french_direct.py`
24
- - `test_french_final_check.py`
25
- - `test_french_simple.sh`
26
- - `test_french_strategies.py`
27
- - `test_generation_fix.sh`
28
- - `test_memory_stress.py`
29
- - `test_quick_french.py`
30
- - `test_service.py`
31
- - `test_system_prompt.py`
32
- - `test_tokenizer_debug.py`
33
- - `test_truncation_issue.py`
34
-
35
- ### Documentation Files (5 files)
36
- Historical documentation superseded by comprehensive final reports.
37
-
38
- ✅ Removed:
39
- - `STATUS.md` (superseded by FINAL_STATUS.md)
40
- - `FIXES_SUMMARY.md` (covered in FINAL_TEST_REPORT.md)
41
- - `PERFORMANCE_REPORT.md` (covered in FINAL_TEST_REPORT.md)
42
- - `memory_test_results.txt` (old test results)
43
- - `test_results.txt` (old test results)
44
-
45
- ### Code Files (2 items)
46
- Debug code not needed in production.
47
-
48
- ✅ Removed:
49
- - `app/routers/debug.py` - Debug endpoint for prompt inspection
50
- - `app/utils/` - Empty directory
51
-
52
- ## Code Changes
53
-
54
- ### Modified: `app/main.py`
55
- **Before:**
56
- ```python
57
- from app.routers import openai_api, debug
58
- ...
59
- app.include_router(debug.router, prefix="/v1")
60
- ```
61
-
62
- **After:**
63
- ```python
64
- from app.routers import openai_api
65
- ...
66
- # Debug router removed
67
- ```
68
-
69
- ### Modified: `README.md`
70
- Updated to reflect:
71
- - Current stable state (production-ready)
72
- - Accurate feature list
73
- - Better API examples with realistic max_tokens
74
- - Chain-of-thought reasoning explanation
75
- - Language support details
76
- - Removed outdated test coverage stats
77
- - Added technical specifications section
78
-
79
- ## Project Structure (After Cleanup)
80
-
81
- ```
82
- simple-llm-pro-finance/
83
- ├── app/ # Core application
84
- │ ├── config.py # Configuration
85
- │ ├── main.py # FastAPI app
86
- │ ├── middleware.py # API key auth
87
- │ ├── models/
88
- │ │ └── openai.py # Pydantic models
89
- │ ├── providers/
90
- │ │ ├── base.py # Provider protocol
91
- │ │ └── transformers_provider.py # Main inference engine
92
- │ ├── routers/
93
- │ │ └── openai_api.py # OpenAI-compatible API
94
- │ └── services/
95
- │ └── chat_service.py # Chat service wrapper
96
- ├── tests/ # Proper test suite
97
- │ ├── conftest.py
98
- │ ├── test_*.py # Unit tests
99
- │ └── performance/ # Performance benchmarks
100
- ├── scripts/ # Utility scripts
101
- │ └── validate_hf_readme.py # README validator
102
- ├── Dockerfile # Docker build config
103
- ├── requirements.txt # Production dependencies
104
- ├── requirements-dev.txt # Development dependencies
105
- ├── README.md # Main documentation
106
- ├── FINAL_STATUS.md # Deployment status
107
- ├── FINAL_TEST_REPORT.md # Test results & metrics
108
- ├── CLEANUP_PLAN.md # This cleanup plan
109
- └── LICENSE # MIT license
110
- ```
111
-
112
- ## Impact Assessment
113
-
114
- ### Breaking Changes
115
- **None** - All removed files were development artifacts.
116
-
117
- ### Removed Endpoints
118
- - `/v1/debug/prompt` - Debug endpoint (never documented in README)
119
-
120
- ### Benefits
121
- - ✅ **Cleaner structure** - 28 fewer files in root directory
122
- - ✅ **Better organization** - Clear separation of concerns
123
- - ✅ **Easier navigation** - No clutter from obsolete scripts
124
- - ✅ **Professional appearance** - Production-ready codebase
125
- - ✅ **Reduced confusion** - No outdated documentation
126
- - ✅ **Smaller repo size** - Faster clones and deployments
127
-
128
- ## Verification
129
-
130
- ### Syntax Validation
131
- ✅ All Python files compile successfully:
132
- - `app/main.py` ✓
133
- - `app/routers/openai_api.py` ✓
134
- - `app/services/chat_service.py` ✓
135
-
136
- ### Import Structure
137
- ✅ No broken imports detected
138
- ✅ All module dependencies satisfied
139
-
140
- ### Test Suite
141
- ✅ Tests remain in `tests/` directory
142
- ✅ Proper pytest structure maintained
143
- ✅ Performance benchmarks preserved
144
-
145
- ## Git Status
146
-
147
- ### Staged Changes (Existing)
148
- - `app/providers/transformers_provider.py` (previous work)
149
- - `quiz_finance_francais.py` (previous work)
150
-
151
- ### Unstaged Changes (This Cleanup)
152
- - Modified: `app/main.py` (removed debug router)
153
- - Modified: `README.md` (updated documentation)
154
- - Deleted: 26 obsolete files
155
- - Added: `CLEANUP_PLAN.md` (this document)
156
-
157
- ## Backup
158
- ✅ Backup branch created: `pre-cleanup-backup`
159
-
160
- To restore if needed:
161
- ```bash
162
- git checkout pre-cleanup-backup
163
- ```
164
-
165
- ## Next Steps
166
-
167
- 1. ✅ Review changes
168
- 2. ⏳ Stage cleanup changes: `git add -A`
169
- 3. ⏳ Commit: `git commit -m "Clean up: Remove obsolete test scripts and documentation"`
170
- 4. ⏳ Optional: Squash with staged changes
171
- 5. ⏳ Push to repository
172
-
173
- ## Success Criteria
174
-
175
- - ✅ All obsolete files removed
176
- - ✅ Code syntax valid
177
- - ✅ No broken imports
178
- - ✅ README updated and accurate
179
- - ✅ Backup created
180
- - ✅ Professional project structure
181
-
182
- ## Summary
183
-
184
- **Removed:** 28 files (21 test scripts, 5 docs, 2 code files)
185
- **Modified:** 2 files (main.py, README.md)
186
- **Added:** 2 files (CLEANUP_PLAN.md, CLEANUP_SUMMARY.md)
187
- **Net Change:** -24 files
188
-
189
- The codebase is now clean, well-organized, and production-ready! 🎉
190
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
CODE_REVIEW_SUMMARY.md DELETED
@@ -1,119 +0,0 @@
1
- # Code Review and Cleanup Summary
2
-
3
- **Date:** November 2, 2025
4
- **Reviewer:** AI Assistant
5
- **Status:** Complete
6
-
7
- ## Executive Summary
8
-
9
- Comprehensive codebase cleanup removing 28 obsolete files and refactoring documentation to be professional and concise.
10
-
11
- ## Changes Made
12
-
13
- ### Files Removed: 28
14
-
15
- **Test Scripts (21 files):**
16
- - All one-off test/debug scripts moved or removed
17
- - Proper tests retained in `tests/` directory
18
-
19
- **Documentation (5 files):**
20
- - Obsolete status reports superseded by final documentation
21
- - Old test result files removed
22
-
23
- **Code (2 items):**
24
- - Debug router removed from production code
25
- - Empty utils directory removed
26
-
27
- ### Files Modified: 2
28
-
29
- **app/main.py:**
30
- - Removed debug router import and mount
31
- - Cleaned up for production deployment
32
-
33
- **README.md:**
34
- - Removed all emojis from section headers
35
- - Eliminated redundant self-congratulatory content
36
- - Condensed from 189 to 139 lines
37
- - Made professional and concise
38
- - Removed "Features" checklist section
39
- - Streamlined technical specifications
40
- - Removed unnecessary "Contributing" section
41
-
42
- ### Files Added: 3
43
-
44
- - `CLEANUP_PLAN.md` - Detailed cleanup strategy
45
- - `CLEANUP_SUMMARY.md` - Execution summary
46
- - `CODE_REVIEW_SUMMARY.md` - This document
47
-
48
- ## Project Structure (After Cleanup)
49
-
50
- ```
51
- simple-llm-pro-finance/
52
- ├── app/ # Application code
53
- │ ├── config.py
54
- │ ├── main.py
55
- │ ├── middleware.py
56
- │ ├── models/
57
- │ ├── providers/
58
- │ ├── routers/
59
- │ └── services/
60
- ├── tests/ # Test suite
61
- ├── scripts/ # Utilities
62
- ├── Dockerfile
63
- ├── requirements.txt
64
- ├── requirements-dev.txt
65
- ├── README.md # Clean, professional docs
66
- ├── FINAL_STATUS.md
67
- ├── FINAL_TEST_REPORT.md
68
- └── LICENSE
69
- ```
70
-
71
- ## Code Quality Improvements
72
-
73
- **Before:**
74
- - 50+ files in repository
75
- - Multiple redundant documentation files
76
- - Debug endpoints in production code
77
- - Verbose, emoji-heavy documentation
78
- - Test scripts scattered in root directory
79
-
80
- **After:**
81
- - 26 essential files
82
- - Single source of truth for documentation
83
- - Production-ready code only
84
- - Professional, concise documentation
85
- - Organized test directory structure
86
-
87
- ## Verification
88
-
89
- - Python syntax validation: PASSED
90
- - Import structure: VALID
91
- - No broken references: CONFIRMED
92
- - Backup created: `pre-cleanup-backup` branch
93
-
94
- ## Impact
95
-
96
- **Breaking Changes:** None
97
- **Removed Endpoints:** `/v1/debug/prompt` (undocumented)
98
- **Repository Size:** Reduced by ~24 files
99
- **Maintainability:** Significantly improved
100
-
101
- ## Recommendations
102
-
103
- ### Immediate
104
- 1. Review and approve changes
105
- 2. Stage all changes: `git add -A`
106
- 3. Commit with message: "refactor: Clean up codebase - remove obsolete files and improve documentation"
107
- 4. Push to repository
108
-
109
- ### Future Considerations
110
- 1. Consider removing `CLEANUP_PLAN.md` and `CLEANUP_SUMMARY.md` after merge
111
- 2. Update `.gitignore` to prevent future test script accumulation
112
- 3. Establish guidelines for temporary debugging files
113
-
114
- ## Conclusion
115
-
116
- The codebase is now clean, professional, and production-ready. All obsolete development artifacts have been removed, documentation is concise and accurate, and the project structure is well-organized.
117
-
118
- **Net Result:** -24 files, cleaner code, better documentation.
119
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
DEPLOYMENT_READY.md DELETED
@@ -1,152 +0,0 @@
1
- # ✅ Deployment Ready - All Critical Issues Fixed
2
-
3
- ## Summary
4
-
5
- All critical issues from the code review have been fixed and new features have been added. The codebase is ready for deployment.
6
-
7
- ## ✅ Completed Tasks
8
-
9
- ### Critical Issues Fixed
10
- - [x] **Model Readiness Check** - Health endpoint now verifies model status
11
- - [x] **Error Sanitization** - Internal errors no longer expose sensitive details
12
- - [x] **Magic Numbers** - All extracted to `constants.py`
13
- - [x] **Duplicate Regex** - Fixed in `open-finance-pydanticAI/app/utils.py`
14
-
15
- ### New Features Added
16
- - [x] **Rate Limiting** - Simple in-memory limiter (30/min, 500/hour)
17
- - [x] **Statistics Tracking** - Comprehensive token and request statistics
18
- - [x] **Stats Endpoint** - `/v1/stats` for monitoring usage
19
- - [x] **Improved Token Counting** - More accurate token tracking
20
-
21
- ### Tests
22
- - [x] **Middleware Tests** - All 5 tests passing ✅
23
- - [x] **Import Issues** - Fixed circular import in middleware package
24
- - [x] **Test Scripts** - Created deployment test scripts
25
-
26
- ## 📁 Files Changed
27
-
28
- ### New Files
29
- - `app/middleware/rate_limit.py` - Rate limiting middleware
30
- - `app/middleware/__init__.py` - Middleware package exports
31
- - `app/utils/stats.py` - Statistics tracking module
32
- - `test_new_features.py` - Python test script
33
- - `test_deployment.sh` - Bash deployment test script
34
- - `DEPLOYMENT_TEST_GUIDE.md` - Testing documentation
35
- - `CHANGES_SUMMARY.md` - Detailed change log
36
-
37
- ### Modified Files
38
- - `app/main.py` - Health check, stats endpoint, middleware setup
39
- - `app/routers/openai_api.py` - Error sanitization
40
- - `app/providers/transformers_provider.py` - Stats tracking, token counting
41
- - `app/utils/constants.py` - New constants added
42
- - `app/middleware.py` - Added `/v1/stats` to public paths
43
- - `open-finance-pydanticAI/app/utils.py` - Fixed duplicate regex
44
-
45
- ## 🚀 Ready to Deploy
46
-
47
- ### Pre-Deployment Checklist
48
- - [x] All critical issues fixed
49
- - [x] Tests passing
50
- - [x] No linting errors
51
- - [x] Documentation updated
52
- - [x] Test scripts created
53
-
54
- ### Deployment Steps
55
-
56
- 1. **Review Changes:**
57
- ```bash
58
- git status
59
- git diff
60
- ```
61
-
62
- 2. **Run Tests Locally (if possible):**
63
- ```bash
64
- # Middleware tests (no model required)
65
- pytest tests/test_middleware.py -v
66
-
67
- # Or use deployment test script
68
- ./test_deployment.sh
69
- ```
70
-
71
- 3. **Commit and Push:**
72
- ```bash
73
- git add .
74
- git commit -m "feat: Add rate limiting, stats tracking, and fix critical issues
75
-
76
- - Add model readiness check to health endpoint
77
- - Sanitize error messages to prevent information leakage
78
- - Extract magic numbers to constants
79
- - Fix duplicate regex in utils
80
- - Add rate limiting (30/min, 500/hour)
81
- - Add comprehensive statistics tracking
82
- - Add /v1/stats endpoint
83
- - Improve token counting accuracy"
84
-
85
- git push origin main
86
- ```
87
-
88
- 4. **Verify Deployment:**
89
- - Check Hugging Face Spaces logs
90
- - Test health endpoint: `curl https://your-space.hf.space/health`
91
- - Test stats endpoint: `curl https://your-space.hf.space/v1/stats`
92
- - Make a test request and verify stats update
93
-
94
- ## 📊 New Endpoints
95
-
96
- ### GET /health
97
- Returns health status with model readiness:
98
- ```json
99
- {
100
- "status": "healthy",
101
- "service": "LLM Pro Finance API",
102
- "model_ready": true
103
- }
104
- ```
105
-
106
- ### GET /v1/stats
107
- Returns comprehensive usage statistics:
108
- ```json
109
- {
110
- "uptime_seconds": 3600,
111
- "total_requests": 50,
112
- "total_tokens": 20000,
113
- "average_total_tokens": 400.0,
114
- "requests_per_hour": 50.0,
115
- "tokens_per_hour": 20000.0,
116
- "requests_by_model": {...},
117
- "tokens_by_model": {...},
118
- "finish_reasons": {...}
119
- }
120
- ```
121
-
122
- ## 🔒 Security Improvements
123
-
124
- - Error messages sanitized (no internal details leaked)
125
- - Rate limiting prevents abuse
126
- - Input validation improved
127
-
128
- ## 📈 Monitoring
129
-
130
- After deployment, monitor:
131
- - Health endpoint for model status
132
- - Stats endpoint for usage patterns
133
- - Rate limiting effectiveness
134
- - Error rates and types
135
-
136
- ## 🎯 Next Steps
137
-
138
- 1. Deploy to Hugging Face Spaces
139
- 2. Run deployment tests
140
- 3. Monitor logs and metrics
141
- 4. Gather user feedback
142
- 5. Consider additional improvements:
143
- - Redis-based rate limiting for multi-server
144
- - Persistent statistics storage
145
- - More detailed monitoring
146
-
147
- ---
148
-
149
- **Status:** ✅ Ready for Deployment
150
- **Date:** 2025-01-30
151
- **All Tests:** Passing ✅
152
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
DEPLOYMENT_TEST_GUIDE.md DELETED
@@ -1,228 +0,0 @@
1
- # Deployment and Testing Guide
2
-
3
- ## Quick Test Summary
4
-
5
- All critical issues have been fixed and new features added. Here's how to test them:
6
-
7
- ## ✅ Changes Made
8
-
9
- 1. **Health Endpoint** - Now includes `model_ready` status
10
- 2. **Error Sanitization** - Internal errors no longer leak details
11
- 3. **Rate Limiting** - 30 req/min, 500 req/hour (demo-friendly)
12
- 4. **Statistics Tracking** - New `/v1/stats` endpoint
13
- 5. **Improved Token Counting** - More accurate token tracking
14
- 6. **Constants Extracted** - All magic numbers moved to constants
15
-
16
- ## 🧪 Testing Options
17
-
18
- ### Option 1: Quick Deployment Test (No Model Required)
19
-
20
- ```bash
21
- # Start server (if not already running)
22
- uvicorn app.main:app --host 0.0.0.0 --port 8080
23
-
24
- # Run deployment test script
25
- ./test_deployment.sh
26
-
27
- # Or test against deployed instance
28
- export API_URL=https://your-space.hf.space
29
- ./test_deployment.sh
30
- ```
31
-
32
- ### Option 2: Python Test Script
33
-
34
- ```bash
35
- # Start server first
36
- uvicorn app.main:app --host 0.0.0.0 --port 8080
37
-
38
- # Run test script
39
- python test_new_features.py
40
- ```
41
-
42
- ### Option 3: Manual Testing
43
-
44
- #### 1. Test Health Endpoint
45
- ```bash
46
- curl http://localhost:8080/health
47
- ```
48
-
49
- **Expected Response:**
50
- ```json
51
- {
52
- "status": "healthy" or "initializing",
53
- "service": "LLM Pro Finance API",
54
- "model_ready": true or false
55
- }
56
- ```
57
-
58
- #### 2. Test Stats Endpoint
59
- ```bash
60
- curl http://localhost:8080/v1/stats
61
- ```
62
-
63
- **Expected Response:**
64
- ```json
65
- {
66
- "uptime_seconds": 3600,
67
- "total_requests": 0,
68
- "total_tokens": 0,
69
- "average_total_tokens": 0.0,
70
- "requests_per_hour": 0.0,
71
- "tokens_per_hour": 0.0,
72
- ...
73
- }
74
- ```
75
-
76
- #### 3. Test Rate Limiting Headers
77
- ```bash
78
- curl -I http://localhost:8080/v1/models
79
- ```
80
-
81
- **Expected Headers:**
82
- ```
83
- X-RateLimit-Limit-Minute: 30
84
- X-RateLimit-Limit-Hour: 500
85
- X-RateLimit-Remaining-Minute: 29
86
- X-RateLimit-Remaining-Hour: 499
87
- ```
88
-
89
- #### 4. Test Error Sanitization
90
- ```bash
91
- curl -X POST http://localhost:8080/v1/chat/completions \
92
- -H "Content-Type: application/json" \
93
- -d '{"model":"test","messages":[]}'
94
- ```
95
-
96
- **Expected:** 400 error with clear message, no internal details
97
-
98
- #### 5. Test Rate Limiting (Trigger 429)
99
- ```bash
100
- # Make 31 requests quickly
101
- for i in {1..31}; do
102
- curl -s http://localhost:8080/v1/models > /dev/null
103
- done
104
- ```
105
-
106
- **Expected:** 31st request returns 429 with `Retry-After` header
107
-
108
- ## 🚀 Deployment to Hugging Face Spaces
109
-
110
- ### Automatic Deployment
111
- If using Hugging Face Spaces, push to the repository and it will auto-deploy:
112
-
113
- ```bash
114
- git add .
115
- git commit -m "feat: Add rate limiting, stats tracking, and fix critical issues"
116
- git push origin main
117
- ```
118
-
119
- ### Manual Verification After Deployment
120
-
121
- 1. **Check Health:**
122
- ```bash
123
- curl https://your-username-open-finance-llm-8b.hf.space/health
124
- ```
125
-
126
- 2. **Check Stats:**
127
- ```bash
128
- curl https://your-username-open-finance-llm-8b.hf.space/v1/stats
129
- ```
130
-
131
- 3. **Make a Test Request:**
132
- ```bash
133
- curl -X POST https://your-username-open-finance-llm-8b.hf.space/v1/chat/completions \
134
- -H "Content-Type: application/json" \
135
- -d '{
136
- "model": "DragonLLM/qwen3-8b-fin-v1.0",
137
- "messages": [{"role": "user", "content": "What is compound interest?"}],
138
- "max_tokens": 500
139
- }'
140
- ```
141
-
142
- 4. **Check Stats Again:**
143
- ```bash
144
- curl https://your-username-open-finance-llm-8b.hf.space/v1/stats
145
- ```
146
- Should show 1 request and token counts.
147
-
148
- ## 📊 What to Verify
149
-
150
- ### ✅ Health Endpoint
151
- - [ ] Returns `model_ready` field
152
- - [ ] Status is "healthy" when model loaded, "initializing" otherwise
153
-
154
- ### ✅ Stats Endpoint
155
- - [ ] Returns comprehensive statistics
156
- - [ ] Token counts increment after requests
157
- - [ ] Request counts increment correctly
158
- - [ ] Averages calculated correctly
159
-
160
- ### ✅ Rate Limiting
161
- - [ ] Headers present in responses
162
- - [ ] 429 returned when limit exceeded
163
- - [ ] `Retry-After` header present on 429
164
- - [ ] Limits reset after time window
165
-
166
- ### ✅ Error Handling
167
- - [ ] Validation errors return 400 with clear messages
168
- - [ ] Internal errors return 500 with sanitized messages
169
- - [ ] No stack traces or file paths in error responses
170
-
171
- ### ✅ Token Counting
172
- - [ ] Token counts in responses match stats
173
- - [ ] Both streaming and non-streaming tracked
174
- - [ ] Token counts are reasonable (not 0 or extremely high)
175
-
176
- ## 🐛 Troubleshooting
177
-
178
- ### Import Errors
179
- If you see import errors, ensure:
180
- - All dependencies installed: `pip install -r requirements.txt`
181
- - Virtual environment activated
182
- - Python path includes project root
183
-
184
- ### Rate Limiting Not Working
185
- - Check middleware is registered in `app/main.py`
186
- - Verify rate limit constants in `app/utils/constants.py`
187
- - Check logs for middleware execution
188
-
189
- ### Stats Not Updating
190
- - Ensure stats tracker is imported in provider
191
- - Check that requests are being recorded
192
- - Verify stats endpoint is accessible (public path)
193
-
194
- ### Health Check Shows "initializing"
195
- - Model may still be loading (check logs)
196
- - Model initialization may have failed (check logs)
197
- - Wait a few minutes and check again
198
-
199
- ## 📝 Test Results Template
200
-
201
- After testing, document results:
202
-
203
- ```
204
- Date: [DATE]
205
- Environment: [Local/Docker/HF Space]
206
- Model Status: [Loaded/Initializing/Failed]
207
-
208
- Health Endpoint: ✅/❌
209
- Stats Endpoint: ✅/❌
210
- Rate Limiting: ✅/❌
211
- Error Handling: ✅/❌
212
- Token Counting: ✅/❌
213
-
214
- Notes:
215
- - [Any issues found]
216
- - [Performance observations]
217
- - [Recommendations]
218
- ```
219
-
220
- ## 🎯 Next Steps
221
-
222
- 1. Run deployment tests
223
- 2. Verify all endpoints work
224
- 3. Test rate limiting behavior
225
- 4. Monitor stats endpoint
226
- 5. Deploy to production
227
- 6. Monitor logs for any issues
228
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
FINAL_STATUS.md DELETED
@@ -1,129 +0,0 @@
1
- # Final Status Report
2
-
3
- ## Issues Investigated
4
-
5
- ### 1. ✅ FIXED: Docker Caching / vLLM → Transformers Migration
6
- **Status:** RESOLVED
7
- - Renamed `vllm.py` → `transformers_provider.py`
8
- - Force-pushed to `main` branch (Space was using `main`, not `master`)
9
- - Added cache-busting in Dockerfile
10
- - **Result:** Space now runs Transformers backend
11
-
12
- ### 2. ✅ FIXED: CUDA Out of Memory Errors
13
- **Status:** RESOLVED
14
- - Added thread-safe initialization with `_init_lock`
15
- - Proper GPU memory cleanup with `torch.cuda.empty_cache()`
16
- - Added `max_memory={0: "20GiB"}` limit during model load
17
- - Added `PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True`
18
- - Memory cleanup in `finally` blocks
19
- - **Result:** No more OOM during initialization, 5/5 sequential requests succeeded
20
-
21
- ### 3. ⚠️ PARTIAL: French Language Support
22
- **Status:** WORKING BUT INCONSISTENT
23
-
24
- **What we discovered:**
25
- - ✅ System prompts ARE being included in the prompt correctly
26
- - Verified with debug endpoint: `<|im_start|>system\nRéponds EN FRANÇAIS<|im_end|>`
27
- - ✅ Chat template is working correctly (custom `chat_template.jinja` loaded)
28
- - ✅ Model CAN produce French answers: "Une obligation est un titre de dette émis par..."
29
- - ❌ Model does NOT always follow system prompts
30
- - ✅ Reasoning (`<think>` tags) is in English (this is normal for Qwen3 architecture)
31
-
32
- **Test results:**
33
- - Question: "Qu'est-ce qu'une obligation?"
34
- Answer: "Une obligation est un titre de dette émis par des États ou des entreprises..." ✅ French
35
-
36
- - Question: "Qu'est-ce qu'une SICAV?"
37
- Answer: "Une **SICAV** (Société d'Investissement à Capital Variable)..." ✅ French
38
-
39
- - Question: "Expliquez le CAC 40"
40
- Answer: "Le **CAC 40** est un indice boursier français qui regroupe..." ✅ French
41
-
42
- **Conclusion:** The model DOES respond in French when French is detected. The automatic French detection + system prompt is working.
43
-
44
- ### 4. ⚠️ IN PROGRESS: Response Truncation
45
- **Status:** IMPROVING
46
-
47
- **Issue:** Responses hitting `max_tokens` limit (finish_reason: length)
48
-
49
- **Why:** Qwen3 uses `<think>` tags for reasoning:
50
- - Reasoning: 300-500 tokens
51
- - Answer: 400-800 tokens
52
- - Total needed: 700-1300 tokens
53
-
54
- **Changes made:**
55
- - Increased default `max_tokens`: 500 → 800 → 1200
56
- - Added proper `finish_reason` detection (was always "stop", now detects "length")
57
- - Added `early_stopping=False` to prevent mid-sentence cutoffs
58
- - Removed `min_new_tokens` constraint
59
-
60
- **Waiting for:** Space rebuild to deploy `max_tokens=1200` default
61
-
62
- ---
63
-
64
- ## Current Status Summary
65
-
66
- | Issue | Status | Notes |
67
- |-------|--------|-------|
68
- | Docker caching | ✅ RESOLVED | Transformers backend deployed |
69
- | OOM errors | ✅ RESOLVED | Memory cleanup working, 5/5 requests succeeded |
70
- | System prompts | ✅ WORKING | Verified in prompt, model partially follows |
71
- | French answers | ✅ WORKING | Model responds in French when detected |
72
- | French reasoning | ⚠️ BY DESIGN | Qwen3 uses English for `<think>` (normal) |
73
- | Truncation | 🔄 IN PROGRESS | Increased max_tokens to 1200, waiting for deployment |
74
-
75
- ---
76
-
77
- ## Key Technical Discoveries
78
-
79
- ### Chat Template
80
- The model has a custom Qwen3 chat template (`chat_template.jinja`) that:
81
- - Uses `<|im_start|>` and `<|im_end|>` tokens
82
- - Supports system/user/assistant roles
83
- - Handles `<think>` tags for reasoning
84
- - **Is being applied correctly** ✅
85
-
86
- ### System Prompt Handling
87
- - System prompts ARE in the generated prompt ✅
88
- - Model follows them **inconsistently** (depends on prompt strength)
89
- - Better strategy: French instruction in user message + system prompt
90
-
91
- ### French Language Capability
92
- - Model **was fine-tuned** on French finance data (LinguaCustodia base)
93
- - Can produce high-quality French financial answers
94
- - Reasoning is in English (Qwen3 architecture design)
95
- - Auto-detection + system prompt is effective
96
-
97
- ---
98
-
99
- ## Recommendations
100
-
101
- ### For French Responses
102
- Current implementation is good:
103
- 1. Auto-detect French from accented characters and patterns ✅
104
- 2. Add French system prompt automatically ✅
105
- 3. Users can also add explicit "Répondez en français" in their question
106
-
107
- ### For Complete Answers
108
- - Default `max_tokens=1200` should handle most cases
109
- - Users can request higher for complex questions
110
- - Clients should check `finish_reason: "length"` for truncation
111
-
112
- ### For Production
113
- - Current setup works well for single-user scenarios
114
- - Consider vLLM for multi-user / high throughput
115
- - L4 GPU provides ~15 tokens/s (typical for 8B models)
116
-
117
- ---
118
-
119
- ## Next Test
120
- Once Space rebuilds with `max_tokens=1200`, run final verification:
121
- ```bash
122
- python test_all_fixes.py
123
- ```
124
-
125
- Expected results:
126
- - ✅ No OOM errors
127
- - ✅ French answers working
128
- - ✅ Minimal truncation (finish_reason: stop)
129
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
FINAL_TEST_REPORT.md DELETED
@@ -1,261 +0,0 @@
1
- # Final Test Report: Finance LLM Deployment
2
-
3
- **Date:** November 2, 2025
4
- **Model:** DragonLLM/qwen3-8b-fin-v1.0
5
- **Backend:** Transformers (PyTorch)
6
- **Hardware:** NVIDIA L4 GPU (24GB VRAM)
7
- **Space:** https://huggingface.co/spaces/jeanbaptdzd/open-finance-llm-8b
8
-
9
- ---
10
-
11
- ## ✅ All Issues Resolved
12
-
13
- ### 1. Docker Caching Issue - **FIXED**
14
- **Problem:** Space was using cached Docker image with old vLLM code
15
- **Root Cause:**
16
- - Branch mismatch (pushing to `master`, Space building from `main`)
17
- - Docker layer caching reused old code
18
- - File `vllm.py` hadn't changed → cache persisted
19
-
20
- **Solution:**
21
- - ✅ Renamed `vllm.py` → `transformers_provider.py` (invalidates cache)
22
- - ✅ Force-pushed correct code to `main` branch
23
- - ✅ Added cache-busting and verification in Dockerfile
24
-
25
- **Result:** Space now runs Transformers backend successfully
26
- ```json
27
- {"backend": "Transformers"} // Previously "vLLM"
28
- ```
29
-
30
- ---
31
-
32
- ### 2. CUDA Out of Memory (OOM) - **FIXED**
33
- **Problem:** Space crashed with CUDA OOM errors after initial deployment
34
- **Root Cause:** No GPU memory cleanup between inference requests, causing memory accumulation
35
-
36
- **Solution:**
37
- - ✅ Added `torch.cuda.empty_cache()` after each inference
38
- - ✅ Added `gc.collect()` for Python garbage collection
39
- - ✅ Proper cleanup in both streaming and non-streaming code paths
40
- - ✅ Moved token counting before cleanup to avoid variable deletion errors
41
-
42
- **Result:** Space runs stably with no memory errors
43
- ```python
44
- # After each inference:
45
- torch.cuda.empty_cache()
46
- gc.collect()
47
- ```
48
-
49
- ---
50
-
51
- ### 3. Truncated Responses - **FIXED**
52
- **Problem:** Responses cut off mid-sentence
53
- **Root Cause:** Qwen3 uses `<think>` tags for reasoning, which consume 40-60% of max_tokens
54
-
55
- **Solution:**
56
- - ✅ Increased max_tokens: 150-200 → 300-600 (based on complexity)
57
- - ✅ Added `min_new_tokens` to ensure minimum generation
58
- - ✅ Fixed `min_new_tokens` formula: was `max_tokens // 2`, now `max_tokens // 10`
59
- - ✅ Added `repetition_penalty=1.05` to prevent loops
60
- - ✅ Added explicit `eos_token_id` handling
61
-
62
- **Result:** All responses complete properly (100% finish_reason=stop)
63
-
64
- ---
65
-
66
- ### 4. French Language Support - **WORKING AS DESIGNED**
67
- **Observation:** French questions show English reasoning in `<think>` tags
68
- **Finding:** This is intentional in Qwen3 models
69
-
70
- **Behavior:**
71
- ```
72
- User: [Question in French]
73
- Model: <think>[Reasoning in English]</think>
74
- [Answer in French]
75
- ```
76
-
77
- **Explanation:**
78
- - Qwen3 is pretrained to use English for internal reasoning
79
- - Maintains consistency and quality across languages
80
- - Final answers are correctly in the requested language
81
- - This is standard behavior for multilingual reasoning models
82
-
83
- ---
84
-
85
- ## 📊 Test Results Summary
86
-
87
- ### English Tests (3/3 Passed - 100%)
88
- | Test | Category | Tokens | Time | Status |
89
- |------|----------|--------|------|--------|
90
- | 1 | Financial Calculations | 300/300 | 20.34s | ✅ |
91
- | 2 | Risk Management (VaR) | 350/350 | 23.43s | ✅ |
92
- | 3 | Options Trading | 300/300 | 20.31s | ✅ |
93
-
94
- ### French Tests (4/4 Passed - 100%)
95
- | Test | Category | Tokens | Time | Status |
96
- |------|----------|--------|------|--------|
97
- | 1 | Calculs Financiers | 300/300 | 20.16s | ✅ |
98
- | 2 | Gestion des Risques (VaR) | 350/350 | 23.48s | ✅ |
99
- | 3 | Options (Call/Put) | 300/300 | 20.25s | ✅ |
100
- | 4 | Termes Français (CAC 40, PEA, etc.) | 400/400 | 27.02s | ✅ |
101
-
102
- ### Overall Performance
103
- - **Success Rate:** 7/7 (100%)
104
- - **Completion Rate:** 7/7 (100% - all finish_reason=stop)
105
- - **Average Speed:** 14.8 tokens/second
106
- - **Average Response Time:** 22.0 seconds
107
- - **Memory Usage:** Stable (no OOM errors)
108
-
109
- ---
110
-
111
- ## 🚀 Performance Characteristics
112
-
113
- ### Inference Speed
114
- - **Tokens/second:** ~14.8 (consistent across all tests)
115
- - **Short responses (50 tokens):** ~3.6s
116
- - **Medium responses (300 tokens):** ~20s
117
- - **Long responses (400 tokens):** ~27s
118
-
119
- ### Memory Management
120
- - **GPU:** NVIDIA L4 (24GB VRAM)
121
- - **Model Size:** Qwen3-8B (8 billion parameters)
122
- - **Memory Efficiency:** Excellent with cleanup
123
- - **Concurrent Requests:** Sequential processing (no batching yet)
124
-
125
- ### Quality
126
- - **Reasoning:** Shows `<think>` tags with step-by-step reasoning
127
- - **Finance Knowledge:** Accurate for VaR, options, compound interest, French market terms
128
- - **Language Support:** English ✅, French ✅ (answers in correct language)
129
- - **Completeness:** 100% of responses finish naturally (finish_reason=stop)
130
-
131
- ---
132
-
133
- ## 🔧 Technical Implementation
134
-
135
- ### Generation Parameters (Optimized)
136
- ```python
137
- {
138
- "max_new_tokens": 300-600, # Increased for reasoning
139
- "min_new_tokens": max(10, max_tokens // 10), # Fixed formula
140
- "temperature": 0.3,
141
- "top_p": 1.0,
142
- "do_sample": True,
143
- "pad_token_id": tokenizer.eos_token_id,
144
- "eos_token_id": tokenizer.eos_token_id,
145
- "repetition_penalty": 1.05
146
- }
147
- ```
148
-
149
- ### Memory Management
150
- ```python
151
- try:
152
- outputs = model.generate(**inputs, **generation_kwargs)
153
- # Process outputs
154
- finally:
155
- del inputs, outputs
156
- torch.cuda.empty_cache()
157
- gc.collect()
158
- ```
159
-
160
- ### Docker Configuration
161
- ```dockerfile
162
- # Cache-busting for fresh builds
163
- ARG CACHE_BUST=20250130_1425
164
- RUN echo "Build cache bust: ${CACHE_BUST}"
165
-
166
- # Code verification
167
- RUN test -f /app/app/providers/transformers_provider.py && \
168
- grep -q "from transformers import" /app/app/providers/transformers_provider.py
169
- ```
170
-
171
- ---
172
-
173
- ## 📝 Key Learnings
174
-
175
- ### 1. Docker Layer Caching in HF Spaces
176
- - File path changes invalidate cache more reliably than content changes
177
- - Renaming files forces fresh rebuild
178
- - Add verification steps in Dockerfile to catch caching issues
179
-
180
- ### 2. GPU Memory Management with PyTorch
181
- - **Must** call `torch.cuda.empty_cache()` after each inference
182
- - Python's `gc.collect()` helps but isn't sufficient alone
183
- - Delete tensors explicitly before cleanup
184
- - Save required values before cleanup (token counts, etc.)
185
-
186
- ### 3. Qwen3 Model Characteristics
187
- - Uses `<think>` tags for chain-of-thought reasoning
188
- - Reasoning consumes 40-60% of token budget
189
- - Needs higher max_tokens than expected (300-600 instead of 150-200)
190
- - Internal reasoning in English even for non-English queries (by design)
191
- - Produces high-quality finance-specific answers
192
-
193
- ### 4. Token Budget Considerations
194
- ```
195
- User prompt: 50 tokens
196
- <think> reasoning: 150-250 tokens (40-60% of max)
197
- Actual answer: 100-200 tokens
198
- Total needed: 300-500 tokens minimum
199
- ```
200
-
201
- ---
202
-
203
- ## ✅ Production Readiness
204
-
205
- ### What's Working
206
- - ✅ Stable inference with no crashes
207
- - ✅ Good response quality (100% completion rate)
208
- - ✅ Proper memory management
209
- - ✅ Multi-language support (English, French)
210
- - ✅ Finance-specific knowledge accurate
211
- - ✅ OpenAI API compatibility
212
-
213
- ### Known Limitations
214
- - ⚠️ Sequential processing only (no request batching)
215
- - ⚠️ ~15 tokens/s (typical for 8B models on L4)
216
- - ⚠️ Reasoning in `<think>` tags always in English
217
- - ⚠️ Token budget must account for reasoning overhead
218
-
219
- ### Recommendations for Production
220
- 1. **For higher throughput:** Consider vLLM backend with continuous batching
221
- 2. **For cost optimization:** Current Transformers backend is fine for <10 users
222
- 3. **For faster inference:** Upgrade to L40s or A100 GPU
223
- 4. **For scaling:** Implement request queuing and load balancing
224
-
225
- ---
226
-
227
- ## 🎯 Next Steps (Optional Improvements)
228
-
229
- ### Performance Optimization
230
- - [ ] Implement vLLM backend for 3-5x speedup with batching
231
- - [ ] Add request queuing for concurrent users
232
- - [ ] Enable tensor parallelism for multi-GPU setups
233
- - [ ] Implement KV cache optimization
234
-
235
- ### User Experience
236
- - [ ] Add option to hide `<think>` tags in responses
237
- - [ ] Implement streaming responses (already supported)
238
- - [ ] Add response time monitoring
239
- - [ ] Create user dashboard with model stats
240
-
241
- ### Advanced Features
242
- - [ ] Fine-tune on additional French finance terminology
243
- - [ ] Add RAG (Retrieval-Augmented Generation) for current market data
244
- - [ ] Implement function calling for calculations
245
- - [ ] Add multi-turn conversation memory
246
-
247
- ---
248
-
249
- ## 📚 References
250
-
251
- - Model: https://huggingface.co/DragonLLM/qwen3-8b-fin-v1.0
252
- - Space: https://huggingface.co/spaces/jeanbaptdzd/open-finance-llm-8b
253
- - Backend: Transformers (PyTorch)
254
- - Hardware: NVIDIA L4 GPU (24GB VRAM)
255
-
256
- ---
257
-
258
- **Status:** ✅ **PRODUCTION READY**
259
- **Last Updated:** November 2, 2025
260
- **Tested by:** Automated test suite (7 comprehensive finance scenarios)
261
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -17,16 +17,26 @@ OpenAI-compatible API powered by DragonLLM/qwen3-8b-fin-v1.0 using Transformers.
17
 
18
  This service provides an OpenAI-compatible API for the DragonLLM Qwen3-8B finance-specialized language model. The model supports both English and French financial terminology and includes chain-of-thought reasoning.
19
 
 
 
 
 
 
 
 
 
 
 
20
  ## API Endpoints
21
 
22
  ### List Models
23
  ```bash
24
- curl -X GET "https://your-username-open-finance-llm-8b.hf.space/v1/models"
25
  ```
26
 
27
  ### Chat Completions
28
  ```bash
29
- curl -X POST "https://your-username-open-finance-llm-8b.hf.space/v1/chat/completions" \
30
  -H "Content-Type: application/json" \
31
  -d '{
32
  "model": "DragonLLM/qwen3-8b-fin-v1.0",
@@ -38,7 +48,7 @@ curl -X POST "https://your-username-open-finance-llm-8b.hf.space/v1/chat/complet
38
 
39
  ### Streaming
40
  ```bash
41
- curl -X POST "https://your-username-open-finance-llm-8b.hf.space/v1/chat/completions" \
42
  -H "Content-Type: application/json" \
43
  -d '{
44
  "model": "DragonLLM/qwen3-8b-fin-v1.0",
@@ -47,11 +57,21 @@ curl -X POST "https://your-username-open-finance-llm-8b.hf.space/v1/chat/complet
47
  }'
48
  ```
49
 
 
 
 
 
 
 
 
 
 
 
50
  ## Response Format
51
 
52
  Responses include chain-of-thought reasoning in `<think>` tags followed by the answer. Reasoning typically consumes 40-60% of tokens.
53
 
54
- Recommended `max_tokens`:
55
  - Simple queries: 300-400
56
  - Complex queries: 500-800
57
  - Detailed analysis: 800-1200
@@ -72,29 +92,50 @@ Recommended `max_tokens`:
72
 
73
  Token priority: `HF_TOKEN_LC2` > `HF_TOKEN_LC` > `HF_TOKEN` > `HUGGING_FACE_HUB_TOKEN`
74
 
75
- Note: Accept model terms at https://huggingface.co/DragonLLM/qwen3-8b-fin-v1.0 before use.
76
 
77
  ## Integration
78
 
79
  ### PydanticAI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  ```python
81
- from pydantic_ai import Agent
82
- from pydantic_ai.models.openai import OpenAIModel
83
 
84
- model = OpenAIModel(
85
- "DragonLLM/qwen3-8b-fin-v1.0",
86
- base_url="https://your-username-open-finance-llm-8b.hf.space/v1"
 
 
 
 
 
 
87
  )
88
- agent = Agent(model=model)
89
  ```
90
 
91
  ### DSPy
 
92
  ```python
93
  import dspy
94
 
95
  lm = dspy.OpenAI(
96
  model="DragonLLM/qwen3-8b-fin-v1.0",
97
- api_base="https://your-username-open-finance-llm-8b.hf.space/v1"
98
  )
99
  ```
100
 
@@ -122,21 +163,41 @@ lm = dspy.OpenAI(
122
  ## Development
123
 
124
  ### Local Setup
 
125
  ```bash
126
  pip install -r requirements.txt
127
  uvicorn app.main:app --reload --port 8080
128
  ```
129
 
130
  ### Testing
 
131
  ```bash
 
132
  pytest -v
133
- pytest --cov=app tests/
 
 
 
 
 
134
  ```
135
 
136
- ## Documentation
137
 
138
- - [FINAL_STATUS.md](FINAL_STATUS.md) - Deployment status
139
- - [FINAL_TEST_REPORT.md](FINAL_TEST_REPORT.md) - Test results and metrics
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
  ## License
142
 
 
17
 
18
  This service provides an OpenAI-compatible API for the DragonLLM Qwen3-8B finance-specialized language model. The model supports both English and French financial terminology and includes chain-of-thought reasoning.
19
 
20
+ ## Features
21
+
22
+ - ✅ **OpenAI-Compatible API** - Drop-in replacement for OpenAI API
23
+ - ✅ **French & English Support** - Automatic language detection
24
+ - ✅ **Rate Limiting** - Built-in protection (30 req/min, 500 req/hour)
25
+ - ✅ **Statistics Tracking** - Token usage and request metrics via `/v1/stats`
26
+ - ✅ **Health Monitoring** - Model readiness status in `/health` endpoint
27
+ - ✅ **Streaming Support** - Real-time response streaming
28
+ - ✅ **PydanticAI Integration** - High-level agent framework included
29
+
30
  ## API Endpoints
31
 
32
  ### List Models
33
  ```bash
34
+ curl -X GET "https://jeanbaptdzd-open-finance-llm-8b.hf.space/v1/models"
35
  ```
36
 
37
  ### Chat Completions
38
  ```bash
39
+ curl -X POST "https://jeanbaptdzd-open-finance-llm-8b.hf.space/v1/chat/completions" \
40
  -H "Content-Type: application/json" \
41
  -d '{
42
  "model": "DragonLLM/qwen3-8b-fin-v1.0",
 
48
 
49
  ### Streaming
50
  ```bash
51
+ curl -X POST "https://jeanbaptdzd-open-finance-llm-8b.hf.space/v1/chat/completions" \
52
  -H "Content-Type: application/json" \
53
  -d '{
54
  "model": "DragonLLM/qwen3-8b-fin-v1.0",
 
57
  }'
58
  ```
59
 
60
+ ### Statistics
61
+ ```bash
62
+ curl -X GET "https://jeanbaptdzd-open-finance-llm-8b.hf.space/v1/stats"
63
+ ```
64
+
65
+ ### Health Check
66
+ ```bash
67
+ curl -X GET "https://jeanbaptdzd-open-finance-llm-8b.hf.space/health"
68
+ ```
69
+
70
  ## Response Format
71
 
72
  Responses include chain-of-thought reasoning in `<think>` tags followed by the answer. Reasoning typically consumes 40-60% of tokens.
73
 
74
+ **Recommended `max_tokens`:**
75
  - Simple queries: 300-400
76
  - Complex queries: 500-800
77
  - Detailed analysis: 800-1200
 
92
 
93
  Token priority: `HF_TOKEN_LC2` > `HF_TOKEN_LC` > `HF_TOKEN` > `HUGGING_FACE_HUB_TOKEN`
94
 
95
+ **Note:** Accept model terms at https://huggingface.co/DragonLLM/qwen3-8b-fin-v1.0 before use.
96
 
97
  ## Integration
98
 
99
  ### PydanticAI
100
+
101
+ The repository includes a PydanticAI integration in `pydanticai_app/`:
102
+
103
+ ```python
104
+ from pydanticai_app.agents import finance_agent
105
+
106
+ result = await finance_agent.run("Qu'est-ce qu'une obligation?")
107
+ ```
108
+
109
+ Or use the FastAPI server:
110
+ ```bash
111
+ uvicorn pydanticai_app.main:app --port 8001
112
+ ```
113
+
114
+ ### OpenAI SDK
115
+
116
  ```python
117
+ from openai import OpenAI
 
118
 
119
+ client = OpenAI(
120
+ base_url="https://jeanbaptdzd-open-finance-llm-8b.hf.space/v1",
121
+ api_key="not-needed"
122
+ )
123
+
124
+ response = client.chat.completions.create(
125
+ model="DragonLLM/qwen3-8b-fin-v1.0",
126
+ messages=[{"role": "user", "content": "What is compound interest?"}],
127
+ max_tokens=500
128
  )
 
129
  ```
130
 
131
  ### DSPy
132
+
133
  ```python
134
  import dspy
135
 
136
  lm = dspy.OpenAI(
137
  model="DragonLLM/qwen3-8b-fin-v1.0",
138
+ api_base="https://jeanbaptdzd-open-finance-llm-8b.hf.space/v1"
139
  )
140
  ```
141
 
 
163
  ## Development
164
 
165
  ### Local Setup
166
+
167
  ```bash
168
  pip install -r requirements.txt
169
  uvicorn app.main:app --reload --port 8080
170
  ```
171
 
172
  ### Testing
173
+
174
  ```bash
175
+ # Run tests
176
  pytest -v
177
+
178
+ # Test deployment
179
+ ./test_deployment.sh
180
+
181
+ # Test PydanticAI integration
182
+ python test_pydanticai.py
183
  ```
184
 
185
+ ## Project Structure
186
 
187
+ ```
188
+ .
189
+ ├── app/ # Main API application
190
+ │ ├── main.py # FastAPI app
191
+ │ ├── routers/ # API routes
192
+ │ ├── providers/ # Model providers
193
+ │ ├── middleware/ # Rate limiting, auth
194
+ │ └── utils/ # Utilities, stats tracking
195
+ ├── pydanticai_app/ # PydanticAI integration
196
+ ├── examples/ # Example scripts
197
+ ├── docs/ # Documentation
198
+ ├── tests/ # Test suite
199
+ └── scripts/ # Utility scripts
200
+ ```
201
 
202
  ## License
203
 
TEST_CODERABBIT.md DELETED
@@ -1,40 +0,0 @@
1
- # Testing CodeRabbit Integration
2
-
3
- ## What to do:
4
-
5
- 1. **Create a branch:**
6
- ```bash
7
- git checkout -b test-coderabbit-review
8
- ```
9
-
10
- 2. **Commit this test file:**
11
- ```bash
12
- git add TEST_CODERABBIT.md .github/pull_request_template.md
13
- git commit -m "test: Add PR template and test CodeRabbit integration"
14
- ```
15
-
16
- 3. **Push and create PR:**
17
- ```bash
18
- git push origin test-coderabbit-review
19
- ```
20
- Then go to GitHub and create a Pull Request from `test-coderabbit-review` to `master`
21
-
22
- 4. **Watch for CodeRabbit:**
23
- - CodeRabbit should automatically comment on your PR
24
- - It will review code quality, suggest improvements
25
- - Check for CodeRabbit comments in the PR thread
26
-
27
- ## What CodeRabbit will review:
28
- - Code quality and best practices
29
- - Potential bugs or security issues
30
- - Performance optimizations
31
- - Documentation completeness
32
- - Test coverage
33
-
34
- ## To test more thoroughly:
35
- After this test, try creating a PR with:
36
- - A small bug (see if it catches it)
37
- - Missing error handling
38
- - Performance issues
39
- - Security concerns
40
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/generation_limits.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Limites de génération - Qwen-3 8B
2
+
3
+ ## Limite théorique maximale
4
+
5
+ **20 000 tokens** peuvent être générés en sortie (selon les spécifications Qwen-3 8B).
6
+
7
+ ## Limite pratique
8
+
9
+ La limite pratique dépend de la **fenêtre de contexte disponible**:
10
+
11
+ ```
12
+ max_tokens_generable = fenêtre_contexte - tokens_entrée - marge_sécurité
13
+ ```
14
+
15
+ ### Exemples pratiques
16
+
17
+ | Contexte d'entrée | Fenêtre totale | Max génération | Marge |
18
+ |-------------------|----------------|----------------|-------|
19
+ | 2K tokens | 32K | ~30K tokens | ✅ Large |
20
+ | 10K tokens | 32K | ~22K tokens | ✅ Bonne |
21
+ | 20K tokens | 32K | ~12K tokens | ✅ Suffisant |
22
+ | 30K tokens | 32K | ~2K tokens | ⚠️ Limite |
23
+ | 50K tokens | 128K (YaRN) | ~78K tokens | ✅ Très large |
24
+
25
+ ## Pour notre application
26
+
27
+ ### Configuration actuelle
28
+ - **max_tokens configuré:** 1500 tokens
29
+ - **Typique contexte entrée:** ~100-500 tokens (messages conversation)
30
+ - **Disponible pour génération:** ~30K tokens
31
+
32
+ ### Pourquoi 1500 tokens est suffisant?
33
+
34
+ 1. **Questions simples:** 800-1000 tokens suffisent
35
+ 2. **Analyses complexes:** 1500 tokens couvrent raisonnement + réponse
36
+ 3. **Messages SWIFT:** 1200-1500 tokens pour format complet
37
+ 4. **Marge de sécurité:** Reste bien en dessous de la limite pratique
38
+
39
+ ## Ajuster max_tokens selon les besoins
40
+
41
+ ### Questions simples (max_tokens=800)
42
+ ```python
43
+ agent_short = Agent(
44
+ finance_model,
45
+ model_settings=ModelSettings(max_output_tokens=800),
46
+ )
47
+ ```
48
+
49
+ ### Analyses complexes (max_tokens=2000)
50
+ ```python
51
+ agent_long = Agent(
52
+ finance_model,
53
+ model_settings=ModelSettings(max_output_tokens=2000),
54
+ )
55
+ ```
56
+
57
+ ### Documents très longs (max_tokens=5000)
58
+ ```python
59
+ agent_very_long = Agent(
60
+ finance_model,
61
+ model_settings=ModelSettings(max_output_tokens=5000),
62
+ )
63
+ # Nécessite que l'entrée soit < 27K tokens
64
+ ```
65
+
66
+ ## Recommandations
67
+
68
+ | Cas d'usage | max_tokens recommandé | Notes |
69
+ |-------------|----------------------|-------|
70
+ | Questions rapides | 800-1000 | Suffisant pour la plupart |
71
+ | Réponses détaillées | 1500-2000 | Inclut raisonnement |
72
+ | Messages SWIFT | 1200-1500 | Format structuré |
73
+ | Analyses longues | 2000-4000 | Si nécessaire |
74
+ | Génération de code/docs | 3000-5000 | Documents complets |
75
+
76
+ **Note:** Au-delà de 5000 tokens, vérifiez que votre contexte d'entrée n'est pas trop volumineux.
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+
docs/qwen3_specifications.md ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Spécifications Qwen-3 8B - Fenêtre de contexte
2
+
3
+ ## Fenêtre de contexte maximale
4
+
5
+ Le modèle **DragonLLM/qwen3-8b-fin-v1.0** (basé sur Qwen-3 8B) supporte:
6
+
7
+ ### Fenêtre de base
8
+ - **32 768 tokens** (32K tokens)
9
+ - Support natif pour la plupart des cas d'usage
10
+
11
+ ### Fenêtre étendue (avec YaRN)
12
+ - **128 000 tokens** (128K tokens)
13
+ - Extension via le mécanisme YaRN (Yet another RoPE extensioN)
14
+ - Nécessite une configuration spécifique pour activer
15
+
16
+ ## Composition du contexte
17
+
18
+ Quand vous envoyez une requête, le contexte total inclut:
19
+
20
+ ```
21
+ Contexte total = Prompt système + Messages conversation + Réponse générée
22
+ ```
23
+
24
+ ### Exemples pratiques:
25
+
26
+ | Type de requête | Prompt + Messages | Réponse max | Total |
27
+ |----------------|-------------------|-------------|-------|
28
+ | Question simple | ~100 tokens | 800 tokens | ~900 tokens |
29
+ | Analyse complexe | ~500 tokens | 1500 tokens | ~2000 tokens |
30
+ | Document long | ~5000 tokens | 2000 tokens | ~7000 tokens |
31
+ | Analyse très longue | ~15000 tokens | 4000 tokens | ~19000 tokens |
32
+
33
+ **Limite pratique recommandée:** 30 000 tokens pour laisser de la marge.
34
+
35
+ ## Limite de génération (max_tokens)
36
+
37
+ **Limite théorique maximale:** **20 000 tokens** en sortie
38
+
39
+ **Limite pratique:** Dépend de la fenêtre de contexte disponible:
40
+ - Si contexte d'entrée = 2K tokens → peut générer jusqu'à ~30K tokens
41
+ - Si contexte d'entrée = 10K tokens → peut générer jusqu'à ~22K tokens
42
+ - Si contexte d'entrée = 30K tokens → peut générer jusqu'à ~2K tokens
43
+
44
+ **Formule:** `max_tokens_generable = fenêtre_contexte - tokens_entrée - marge_sécurité`
45
+
46
+ ## Configuration actuelle
47
+
48
+ Dans notre application PydanticAI:
49
+ - `max_tokens` (génération): **1500 tokens** (configurable)
50
+ - Contexte d'entrée: Illimité jusqu'à ~30K tokens (pour laisser de la marge)
51
+ - Contexte total: Jusqu'à 32K tokens (base) ou 128K (avec YaRN)
52
+ - Limite théorique max: 20K tokens en sortie (mais contrainte par contexte disponible)
53
+
54
+ ## Recommandations
55
+
56
+ ### Pour des requêtes simples:
57
+ ```python
58
+ max_tokens = 800-1000 # Suffisant pour la plupart des réponses
59
+ ```
60
+
61
+ ### Pour des requêtes complexes (SWIFT, analyses):
62
+ ```python
63
+ max_tokens = 1500-2000 # Permet raisonnement + réponse complète
64
+ ```
65
+
66
+ ### Pour des documents longs:
67
+ - Utilisez le contexte jusqu'à ~30K tokens pour le prompt
68
+ - Réservez 2-5K tokens pour la réponse
69
+ - Total: jusqu'à 32K tokens (base)
70
+
71
+ ### Activation de YaRN pour contexte étendu:
72
+ Si vous avez besoin de plus de 32K tokens:
73
+ 1. Vérifiez que le backend Transformers supporte YaRN
74
+ 2. Configurez les paramètres de RoPE scaling
75
+ 3. La fenêtre peut être étendue jusqu'à 128K tokens
76
+
77
+ ## Références
78
+
79
+ - Qwen-3 models: Fenêtre de 32K tokens (base), 128K avec YaRN
80
+ - YaRN: Yet another RoPE extensioN - méthode d'extension de contexte
81
+ - Documentation technique Qwen: https://huggingface.co/Qwen/Qwen2.5
82
+
docs/reasoning_models.md ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gestion des modèles de raisonnement avec PydanticAI
2
+
3
+ ## Problème: "finish on length"
4
+
5
+ Quand vous voyez `finish_reason: "length"`, cela signifie que le modèle a atteint la limite de `max_tokens` avant de terminer sa réponse.
6
+
7
+ ## Pourquoi c'est fréquent avec les modèles de raisonnement?
8
+
9
+ Les modèles comme Qwen3 utilisent des balises `<think>` (ou `<think>`) pour le raisonnement en chaîne:
10
+
11
+ ```
12
+ <think>
13
+ 1. L'utilisateur demande un message SWIFT MT103
14
+ 2. Je dois identifier les champs requis
15
+ 3. Format: :20: référence, :32A: date/devise/montant...
16
+ </think>
17
+
18
+ Voici le message SWIFT généré:
19
+ :20:NONREF
20
+ :23B:CRED
21
+ ...
22
+ ```
23
+
24
+ **Le raisonnement peut consommer 40-60% du budget de tokens!**
25
+
26
+ ## Solution: Augmenter max_tokens
27
+
28
+ Nous avons configuré `max_tokens=1500` dans `app/config.py` pour permettre:
29
+ - ~600-900 tokens pour le raisonnement (`<think>` tags)
30
+ - ~600-900 tokens pour la réponse finale
31
+ - Total: ~1500 tokens pour des réponses complètes
32
+
33
+ ## Configuration actuelle
34
+
35
+ ```python
36
+ # app/config.py
37
+ max_tokens: int = 1500 # Pour modèles de raisonnement
38
+
39
+ # app/models.py
40
+ model_settings = ModelSettings(
41
+ max_output_tokens=settings.max_tokens,
42
+ )
43
+ finance_model = OpenAIModel(
44
+ ...,
45
+ model_settings=model_settings,
46
+ )
47
+ ```
48
+
49
+ ## Recommandations par type de requête
50
+
51
+ | Type de requête | max_tokens recommandé |
52
+ |----------------|----------------------|
53
+ | Questions simples | 800-1000 |
54
+ | Génération SWIFT | 1200-1500 |
55
+ | Analyse complexe | 1500-2000 |
56
+ | Extraction structurée | 1000-1200 |
57
+
58
+ ## Comment ajuster pour un agent spécifique?
59
+
60
+ Vous pouvez créer des agents avec des settings différents:
61
+
62
+ ```python
63
+ from pydantic_ai import ModelSettings, Agent
64
+
65
+ # Agent pour tâches courtes
66
+ short_agent = Agent(
67
+ finance_model,
68
+ model_settings=ModelSettings(max_output_tokens=800),
69
+ system_prompt="..."
70
+ )
71
+
72
+ # Agent pour tâches longues (SWIFT, analyses)
73
+ long_agent = Agent(
74
+ finance_model,
75
+ model_settings=ModelSettings(max_output_tokens=2000),
76
+ system_prompt="..."
77
+ )
78
+ ```
79
+
80
+ ## Vérifier si la réponse est complète
81
+
82
+ Notre utilitaire `extract_answer_from_reasoning()` dans `app/utils.py` gère automatiquement:
83
+ - Extraction de la réponse après les balises `<think>`
84
+ - Détection si la réponse est tronquée
85
+ - Nettoyage des balises de raisonnement
86
+
87
+
88
+
89
+
90
+
91
+
92
+
93
+
94
+
examples/README.md ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Exemples d'Agentique avec PydanticAI
2
+
3
+ Ces exemples démontrent différentes capacités agentiques de PydanticAI utilisant le modèle DragonLLM via le Hugging Face Space.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ cd /Users/jeanbapt/open-finance-pydanticAI
9
+ pip install -e ".[dev]"
10
+ ```
11
+
12
+ ## Exemples
13
+
14
+ ### Agent 1: Extraction de données structurées
15
+ **Fichier:** `agent_1_structured_data.py`
16
+
17
+ Démontre l'extraction et la validation de données financières structurées à partir de textes non structurés.
18
+
19
+ **Fonctionnalités:**
20
+ - Utilisation de `output_type` avec modèles Pydantic
21
+ - Validation automatique des données
22
+ - Extraction d'informations complexes (portfolios, transactions)
23
+
24
+ **Exécution:**
25
+ ```bash
26
+ python examples/agent_1_structured_data.py
27
+ ```
28
+
29
+ ### Agent 2: Agent avec outils (Tools)
30
+ **Fichier:** `agent_2_tools.py`
31
+
32
+ Démontre l'utilisation d'outils Python que l'agent peut appeler pour effectuer des calculs.
33
+
34
+ **Fonctionnalités:**
35
+ - Définition d'outils Python (fonctions)
36
+ - Appel automatique d'outils par l'agent
37
+ - Combinaison de raisonnement LLM + calculs précis
38
+
39
+ **Outils disponibles:**
40
+ - `calculer_valeur_future()` - Intérêts composés
41
+ - `calculer_versement_mensuel()` - Prêts immobiliers
42
+ - `calculer_performance_portfolio()` - Performance d'investissements
43
+
44
+ **Exécution:**
45
+ ```bash
46
+ python examples/agent_2_tools.py
47
+ ```
48
+
49
+ ### Agent 4: Outils et mémoire
50
+ **Fichier:** `agent_with_tools_and_memory.py`
51
+
52
+ Démontre l'utilisation combinée d'outils Python et de mémoire (History) pour créer des agents conversationnels intelligents.
53
+
54
+ **Fonctionnalités:**
55
+ - Outils financiers intégrés (calculs précis)
56
+ - Mémoire conversationnelle (History)
57
+ - Agents qui se souviennent du contexte
58
+ - Conseils personnalisés basés sur l'historique
59
+
60
+ **Outils disponibles:**
61
+ - `calculer_valeur_future()` - Intérêts composés
62
+ - `calculer_versement_mensuel()` - Prêts immobiliers
63
+ - `calculer_performance_portfolio()` - Performance d'investissements
64
+ - `calculer_ratio_dette()` - Analyse d'endettement
65
+
66
+ **Exécution:**
67
+ ```bash
68
+ python examples/agent_with_tools_and_memory.py
69
+ ```
70
+
71
+ ### Agent 5: Stratégies de mémoire
72
+ **Fichier:** `memory_strategies.py`
73
+
74
+ Démontre différentes stratégies de gestion de mémoire pour optimiser les performances et la persistance.
75
+
76
+ **Stratégies:**
77
+ 1. Mémoire simple (History) - Tout est conservé
78
+ 2. Mémoire sélective - Extraction de faits clés
79
+ 3. Mémoire structurée - Profil client typé
80
+ 4. Mémoire avec résumé - Compression périodique
81
+ 5. Mémoire persistante - Sauvegarde/chargement multi-session
82
+
83
+ **Exécution:**
84
+ ```bash
85
+ python examples/memory_strategies.py
86
+ ```
87
+
88
+ ### Agent 3: Workflow multi-étapes
89
+ **Fichier:** `agent_3_multi_step.py`
90
+
91
+ Démontre la création d'un workflow où plusieurs agents spécialisés collaborent.
92
+
93
+ **Fonctionnalités:**
94
+ - Agents spécialisés (analyse de risque, fiscalité, optimisation)
95
+ - Passage de contexte entre agents
96
+ - Orchestration de workflows complexes
97
+
98
+ **Agents:**
99
+ - `risk_analyst` - Analyse de risque financier
100
+ - `tax_advisor` - Conseil fiscal français
101
+ - `portfolio_optimizer` - Optimisation de portfolio
102
+
103
+ **Exécution:**
104
+ ```bash
105
+ python examples/agent_3_multi_step.py
106
+ ```
107
+
108
+ ## Points clés démontrés
109
+
110
+ 1. **Extraction structurée**: PydanticAI peut extraire et valider des données complexes
111
+ 2. **Outils intégrés**: Les agents peuvent appeler des fonctions Python pour des calculs précis
112
+ 3. **Multi-agents**: Plusieurs agents peuvent collaborer pour résoudre des problèmes complexes
113
+ 4. **Raisonnement**: Le modèle Qwen3 fournit le raisonnement via les balises `<think>`
114
+
115
+ ## Cas d'usage réels
116
+
117
+ Ces exemples peuvent être adaptés pour:
118
+ - **Analyse de documents financiers**: Extraction automatique de données de contrats, factures
119
+ - **Calculs financiers interactifs**: Assistants qui calculent en temps réel
120
+ - **Conseil financier automatisé**: Workflows d'analyse multi-domaines
121
+
examples/SWIFT_IMPROVEMENTS.md ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Améliorations de l'extraction SWIFT
2
+
3
+ ## Résumé des améliorations
4
+
5
+ L'extraction de messages SWIFT a été complètement révisée et améliorée avec:
6
+
7
+ ### 1. Parser robuste avec validation Pydantic
8
+
9
+ **Fichier:** `swift_extractor.py`
10
+
11
+ - Nouveau module dédié à l'extraction SWIFT avec validation stricte
12
+ - Utilisation de modèles Pydantic pour garantir la cohérence des données
13
+ - Validation automatique des formats (dates, devises, montants, BIC)
14
+
15
+ ### 2. Support complet des champs SWIFT MT103
16
+
17
+ **Champs gérés:**
18
+ - `:20:` - Référence du transfert
19
+ - `:23B:` - Code instruction (CRED, etc.)
20
+ - `:32A:` - Date de valeur, devise, montant (avec parsing intelligent)
21
+ - `:50K:`, `:50A:`, `:50F:` - Ordre donneur (multi-lignes)
22
+ - `:52A:`, `:52D:` - Banque ordonnateur
23
+ - `:56A:`, `:56D:` - Banque intermédiaire
24
+ - `:57A:`, `:57D:` - Banque bénéficiaire
25
+ - `:59:`, `:59A:` - Bénéficiaire (multi-lignes)
26
+ - `:70:` - Information pour bénéficiaire (multi-lignes)
27
+ - `:71A:` - Frais (OUR/SHA/BEN)
28
+ - `:72:` - Information banque à banque (multi-lignes)
29
+
30
+ ### 3. Gestion des champs multi-lignes
31
+
32
+ Le parser gère correctement les champs qui s'étendent sur plusieurs lignes:
33
+ - Lire toutes les lignes jusqu'au prochain tag SWIFT
34
+ - Préserver les sauts de ligne dans les adresses et noms
35
+ - Extraire les informations structurées (IBAN, BIC) depuis le texte libre
36
+
37
+ ### 4. Extraction automatique
38
+
39
+ **IBAN:**
40
+ - Détection automatique des IBAN dans les champs `:50K:` et `:59:`
41
+ - Validation de la longueur (15-34 caractères)
42
+ - Nettoyage automatique (suppression des espaces)
43
+
44
+ **BIC:**
45
+ - Extraction depuis les champs `:52A:`, `:56A:`, `:57A:`
46
+ - Validation du format (8 ou 11 caractères)
47
+ - Pattern matching robuste
48
+
49
+ ### 5. Support des formats de date
50
+
51
+ **Format :32A:**
52
+ - Support YYMMDD (6 chiffres) → conversion automatique en YYYYMMDD
53
+ - Support YYYYMMDD (8 chiffres)
54
+ - Logique intelligente pour les années (YY < 50 → 20YY, sinon 19YY)
55
+
56
+ ### 6. Validation stricte
57
+
58
+ **Validations implémentées:**
59
+ - Dates: format YYYYMMDD avec vérification des valeurs
60
+ - Devises: codes ISO 3 lettres majuscules
61
+ - Montants: nombres positifs avec gestion des virgules/points
62
+ - BIC: longueur 8 ou 11 caractères
63
+ - Charges: valeurs strictes (OUR, SHA, BEN)
64
+
65
+ ### 7. Structure de données typée
66
+
67
+ **Modèle Pydantic:** `SwiftMT103Parsed`
68
+
69
+ ```python
70
+ class SwiftMT103Parsed(BaseModel):
71
+ field_20: str # Référence
72
+ field_32A: SwiftField32A # Date, devise, montant (validé)
73
+ field_50K: str # Ordre donneur
74
+ field_59: str # Bénéficiaire
75
+ # ... tous les champs optionnels
76
+ ordering_customer_account: Optional[str] # IBAN extrait
77
+ beneficiary_account: Optional[str] # IBAN extrait
78
+ ```
79
+
80
+ ### 8. Fonctionnalités supplémentaires
81
+
82
+ **Formatage inverse:**
83
+ - `format_swift_mt103_from_parsed()` - Reconstitution du message SWIFT depuis une structure parsée
84
+
85
+ **Gestion d'erreurs:**
86
+ - Messages d'erreur détaillés pour faciliter le débogage
87
+ - Fallback vers extraction LLM si le parsing échoue
88
+
89
+ ## Utilisation
90
+
91
+ ### Parser basique (ancienne fonction)
92
+
93
+ ```python
94
+ from examples.agent_swift import parse_swift_mt103
95
+
96
+ swift_text = """
97
+ :20:NONREF
98
+ :23B:CRED
99
+ :32A:241215EUR15000.00
100
+ :50K:/FR76300040000100000000000123
101
+ ORDRE DUPONT JEAN
102
+ :59:/FR1420041010050500013M02606
103
+ BENEFICIAIRE MARTIN
104
+ :71A:OUR
105
+ """
106
+
107
+ parsed = parse_swift_mt103(swift_text)
108
+ ```
109
+
110
+ ### Parser avancé (recommandé)
111
+
112
+ ```python
113
+ from examples.swift_extractor import parse_swift_mt103_advanced
114
+
115
+ parsed = parse_swift_mt103_advanced(swift_text)
116
+
117
+ # Accès aux données validées
118
+ print(parsed.field_32A.amount) # 15000.0
119
+ print(parsed.field_32A.currency) # EUR
120
+ print(parsed.field_32A.value_date) # 20241215
121
+ print(parsed.ordering_customer_account) # FR76300040000100000000000123
122
+ ```
123
+
124
+ ### Avec agent PydanticAI
125
+
126
+ ```python
127
+ from examples.agent_swift import swift_parser
128
+
129
+ result = await swift_parser.run(f"Parse ce message SWIFT:\n{swift_text}")
130
+ # L'agent utilise le parser avancé en arrière-plan
131
+ ```
132
+
133
+ ## Améliorations futures possibles
134
+
135
+ 1. **Support MT940** (relevés bancaires)
136
+ 2. **Support MT202** (transferts interbancaires)
137
+ 3. **Validation IBAN** (algorithme de contrôle)
138
+ 4. **Cache de parsing** pour performance
139
+ 5. **Mode strict vs permissif** pour différents niveaux de validation
140
+
141
+ ## Tests
142
+
143
+ Tous les parsers sont testés avec:
144
+ - Messages SWIFT standards
145
+ - Formats YYMMDD et YYYYMMDD
146
+ - Champs multi-lignes complexes
147
+ - Champs optionnels
148
+ - Cas limites (montants avec virgules, IBAN avec espaces, etc.)
149
+
150
+
151
+
152
+
153
+
154
+
155
+
156
+
157
+
examples/agent_1_structured_data.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agent 1: Extraction et validation de données financières structurées
3
+
4
+ Cet agent démontre l'utilisation de PydanticAI pour extraire et valider
5
+ des données structurées à partir de textes financiers non structurés.
6
+ """
7
+
8
+ import asyncio
9
+ from pydantic import BaseModel, Field
10
+ from pydantic_ai import Agent, ModelSettings
11
+
12
+ from app.models import finance_model
13
+
14
+
15
+ # Modèles de données structurées
16
+ class PositionBoursiere(BaseModel):
17
+ """Représente une position boursière."""
18
+ symbole: str = Field(description="Symbole de l'action (ex: AIR.PA, SAN.PA)")
19
+ quantite: int = Field(description="Nombre d'actions", ge=0)
20
+ prix_achat: float = Field(description="Prix d'achat unitaire en euros", ge=0)
21
+ date_achat: str = Field(description="Date d'achat au format YYYY-MM-DD")
22
+
23
+
24
+ class Portfolio(BaseModel):
25
+ """Portfolio avec positions boursières."""
26
+ positions: list[PositionBoursiere] = Field(description="Liste des positions")
27
+ valeur_totale: float = Field(description="Valeur totale du portfolio en euros", ge=0)
28
+ date_evaluation: str = Field(description="Date d'évaluation")
29
+
30
+
31
+ # Agent pour extraction de données structurées
32
+ extract_agent = Agent(
33
+ finance_model,
34
+ model_settings=ModelSettings(max_output_tokens=1200), # Sufficient for structured data extraction
35
+ system_prompt=(
36
+ "Vous êtes un assistant expert en analyse de données financières. "
37
+ "Votre rôle est d'extraire des informations structurées à partir "
38
+ "de textes non structurés concernant des portfolios d'actions françaises. "
39
+ "Identifiez les symboles, quantités, prix d'achat et dates. "
40
+ "Calculez la valeur totale du portfolio."
41
+ ),
42
+ )
43
+
44
+
45
+ async def exemple_extraction_portfolio():
46
+ """Exemple d'extraction de données de portfolio."""
47
+ texte_non_structure = """
48
+ Mon portfolio actuel :
49
+ - J'ai acheté 50 actions Airbus (AIR.PA) à 120€ le 15 mars 2024
50
+ - 30 actions Sanofi (SAN.PA) à 85€ le 20 février 2024
51
+ - 100 actions TotalEnergies (TTE.PA) à 55€ le 10 janvier 2024
52
+
53
+ Date d'évaluation : 1er novembre 2024
54
+ """
55
+
56
+ print("📊 Agent 1: Extraction de données structurées")
57
+ print("=" * 60)
58
+ print(f"Texte d'entrée:\n{texte_non_structure}\n")
59
+
60
+ result = await extract_agent.run(
61
+ f"Extrais les informations du portfolio suivant et formate-les de manière structurée:\n{texte_non_structure}\n\n"
62
+ "Réponds avec:\n- Le nombre de positions\n- Les détails de chaque position (symbole, quantité, prix, date)\n- La valeur totale estimée"
63
+ )
64
+
65
+ # Parser la réponse texte (simplifié pour l'exemple)
66
+ response = result.output
67
+ # En production, on utiliserait output_type=Portfolio pour validation automatique
68
+ print("✅ Résultat structuré:")
69
+ print(response)
70
+ print("\n💡 Note: Avec output_type=Portfolio, PydanticAI validerait")
71
+ print(" automatiquement la structure et fournirait un objet typé.")
72
+
73
+ return response
74
+
75
+
76
+ if __name__ == "__main__":
77
+ asyncio.run(exemple_extraction_portfolio())
78
+
examples/agent_2_tools.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agent 2: Agent avec outils (Tools) pour calculs financiers
3
+
4
+ Cet agent démontre l'utilisation d'outils Python que l'agent peut appeler
5
+ pour effectuer des calculs financiers complexes.
6
+ """
7
+
8
+ import asyncio
9
+ from typing import Annotated
10
+ from pydantic import BaseModel
11
+ from pydantic_ai import Agent, ModelSettings
12
+
13
+ from app.models import finance_model
14
+
15
+
16
+ # Outils que l'agent peut utiliser
17
+ def calculer_valeur_future(
18
+ capital_initial: float,
19
+ taux_annuel: float,
20
+ duree_annees: float
21
+ ) -> str:
22
+ """Calcule la valeur future avec intérêts composés.
23
+
24
+ Args:
25
+ capital_initial: Montant initial en euros
26
+ taux_annuel: Taux d'intérêt annuel (ex: 0.05 pour 5%)
27
+ duree_annees: Durée en années
28
+
29
+ Returns:
30
+ Valeur future calculée
31
+ """
32
+ valeur_future = capital_initial * (1 + taux_annuel) ** duree_annees
33
+ interets = valeur_future - capital_initial
34
+ return (
35
+ f"Valeur future: {valeur_future:,.2f}€\n"
36
+ f"Intérêts générés: {interets:,.2f}€\n"
37
+ f"Capital initial: {capital_initial:,.2f}€"
38
+ )
39
+
40
+
41
+ def calculer_versement_mensuel(
42
+ capital_emprunte: float,
43
+ taux_annuel: float,
44
+ duree_mois: int
45
+ ) -> str:
46
+ """Calcule le versement mensuel pour un prêt.
47
+
48
+ Args:
49
+ capital_emprunte: Montant emprunté en euros
50
+ taux_annuel: Taux d'intérêt annuel (ex: 0.04 pour 4%)
51
+ duree_mois: Durée du prêt en mois
52
+
53
+ Returns:
54
+ Versement mensuel calculé
55
+ """
56
+ taux_mensuel = taux_annuel / 12
57
+ versement = capital_emprunte * (
58
+ taux_mensuel * (1 + taux_mensuel) ** duree_mois
59
+ ) / ((1 + taux_mensuel) ** duree_mois - 1)
60
+
61
+ total_rembourse = versement * duree_mois
62
+ cout_total = total_rembourse - capital_emprunte
63
+
64
+ return (
65
+ f"Versement mensuel: {versement:,.2f}€\n"
66
+ f"Total remboursé: {total_rembourse:,.2f}€\n"
67
+ f"Coût total du crédit: {cout_total:,.2f}€"
68
+ )
69
+
70
+
71
+ def calculer_performance_portfolio(
72
+ valeur_initiale: float,
73
+ valeur_actuelle: float,
74
+ duree_jours: int
75
+ ) -> str:
76
+ """Calcule la performance d'un portfolio.
77
+
78
+ Args:
79
+ valeur_initiale: Valeur initiale en euros
80
+ valeur_actuelle: Valeur actuelle en euros
81
+ duree_jours: Durée en jours
82
+
83
+ Returns:
84
+ Performance calculée
85
+ """
86
+ gain_absolu = valeur_actuelle - valeur_initiale
87
+ gain_pourcentage = (gain_absolu / valeur_initiale) * 100
88
+ rendement_annuelise = ((valeur_actuelle / valeur_initiale) ** (365 / duree_jours) - 1) * 100
89
+
90
+ return (
91
+ f"Gain absolu: {gain_absolu:+,.2f}€ ({gain_pourcentage:+.2f}%)\n"
92
+ f"Rendement annualisé: {rendement_annuelise:+.2f}%\n"
93
+ f"Durée: {duree_jours} jours"
94
+ )
95
+
96
+
97
+ # Agent avec outils
98
+ finance_calculator_agent = Agent(
99
+ finance_model,
100
+ model_settings=ModelSettings(max_output_tokens=1500), # For explanations with calculations
101
+ system_prompt=(
102
+ "Vous êtes un conseiller financier expert. "
103
+ "Quand un client vous pose une question nécessitant un calcul financier, "
104
+ "utilisez les outils de calcul disponibles pour fournir des résultats précis. "
105
+ "Expliquez toujours les résultats dans le contexte de la question du client. "
106
+ "Répondez en français."
107
+ ),
108
+ tools=[calculer_valeur_future, calculer_versement_mensuel, calculer_performance_portfolio],
109
+ )
110
+
111
+
112
+ async def exemple_agent_avec_outils():
113
+ """Exemple d'utilisation d'un agent avec outils."""
114
+ print("\n🔧 Agent 2: Agent avec outils de calcul")
115
+ print("=" * 60)
116
+
117
+ question = (
118
+ "J'ai un capital de 50 000€ que je veux placer à 4% par an pendant 10 ans. "
119
+ "Combien aurai-je à la fin ? Et si j'emprunte 200 000€ sur 20 ans à 3.5% "
120
+ "pour acheter un appartement, combien paierai-je par mois ?"
121
+ )
122
+
123
+ print(f"Question:\n{question}\n")
124
+
125
+ result = await finance_calculator_agent.run(question)
126
+
127
+ print("✅ Réponse de l'agent avec calculs:")
128
+ print(result.output)
129
+ print()
130
+
131
+ # Afficher quels outils ont été utilisés
132
+ if hasattr(result, 'usage') and result.usage:
133
+ print("📊 Utilisation des outils:")
134
+ print(f" - Tokens utilisés: {result.usage.total_tokens if hasattr(result.usage, 'total_tokens') else 'N/A'}")
135
+
136
+
137
+ if __name__ == "__main__":
138
+ asyncio.run(exemple_agent_avec_outils())
139
+
examples/agent_3_multi_step.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agent 3: Workflow multi-étapes avec agents spécialisés
3
+
4
+ Cet agent démontre la création d'un workflow où plusieurs agents spécialisés
5
+ collaborent pour résoudre un problème financier complexe.
6
+ """
7
+
8
+ import asyncio
9
+ from pydantic import BaseModel, Field
10
+ from pydantic_ai import Agent, ModelSettings
11
+
12
+ from app.models import finance_model
13
+
14
+
15
+ # Agents spécialisés avec limites appropriées
16
+ risk_analyst = Agent(
17
+ finance_model,
18
+ model_settings=ModelSettings(max_output_tokens=1200), # Risk analysis
19
+ system_prompt=(
20
+ "Vous êtes un analyste de risque financier. "
21
+ "Vous évaluez les risques associés à différents instruments financiers "
22
+ "et stratégies d'investissement. "
23
+ "Fournissez une évaluation de risque sur 5 niveaux (1=très faible, 5=très élevé)."
24
+ ),
25
+ )
26
+
27
+ tax_advisor = Agent(
28
+ finance_model,
29
+ model_settings=ModelSettings(max_output_tokens=1500), # Tax advice can be detailed
30
+ system_prompt=(
31
+ "Vous êtes un conseiller fiscal français. "
32
+ "Vous expliquez les implications fiscales des investissements "
33
+ "selon la réglementation française (PEA, assurance-vie, compte-titres, etc.)."
34
+ ),
35
+ )
36
+
37
+ portfolio_optimizer = Agent(
38
+ finance_model,
39
+ model_settings=ModelSettings(max_output_tokens=2000), # Portfolio optimization can be complex
40
+ system_prompt=(
41
+ "Vous êtes un optimiseur de portfolio. "
42
+ "Vous proposez des allocations d'actifs optimisées "
43
+ "en fonction des objectifs, de l'horizon temporel et du profil de risque. "
44
+ "Répondez toujours en français."
45
+ ),
46
+ )
47
+
48
+
49
+ class AnalyseRisque(BaseModel):
50
+ """Analyse de risque."""
51
+ niveau_risque: int = Field(description="Niveau de risque de 1 à 5", ge=1, le=5)
52
+ facteurs_risque: list[str] = Field(description="Liste des facteurs de risque identifiés")
53
+ recommandation: str = Field(description="Recommandation basée sur le niveau de risque")
54
+
55
+
56
+ async def workflow_analyse_investissement():
57
+ """Workflow multi-étapes pour analyser un investissement."""
58
+ print("\n🔄 Agent 3: Workflow multi-étapes")
59
+ print("=" * 60)
60
+
61
+ scenario = """
62
+ Un investisseur de 35 ans avec un profil modéré souhaite investir 100 000€.
63
+ Objectif: Préparer la retraite dans 30 ans.
64
+ Il envisage:
65
+ - 40% en actions françaises (CAC 40)
66
+ - 30% en obligations d'État
67
+ - 20% en immobiler via SCPI
68
+ - 10% en cryptomonnaies
69
+
70
+ Analysez ce portfolio du point de vue:
71
+ 1. Risque
72
+ 2. Fiscalité
73
+ 3. Optimisation
74
+ """
75
+
76
+ print("Scénario:\n", scenario, "\n")
77
+
78
+ # Étape 1: Analyse de risque
79
+ print("📊 Étape 1: Analyse de risque...")
80
+ risk_result = await risk_analyst.run(
81
+ f"Analyse le niveau de risque (1-5) de cette stratégie:\n{scenario}\n\n"
82
+ "Fournis: niveau de risque (1-5), facteurs de risque principaux, et recommandation."
83
+ )
84
+ risk_output = risk_result.output
85
+ print(f" Analyse:\n {risk_output[:300]}...\n")
86
+
87
+ # Étape 2: Conseil fiscal
88
+ print("💰 Étape 2: Analyse fiscale...")
89
+ tax_result = await tax_advisor.run(
90
+ f"Quelles sont les implications fiscales de cette stratégie d'investissement "
91
+ f"en France?\n{scenario}"
92
+ )
93
+ print(f" Conseil fiscal:\n {tax_result.output[:300]}...\n")
94
+
95
+ # Étape 3: Optimisation avec contexte des étapes précédentes
96
+ print("🎯 Étape 3: Optimisation du portfolio...")
97
+ optimization_result = await portfolio_optimizer.run(
98
+ f"""
99
+ Scénario: {scenario}
100
+
101
+ Analyses précédentes:
102
+ - Analyse de risque: {risk_output[:200]}
103
+ - Analyse fiscale: {tax_result.output[:200]}
104
+
105
+ Propose une allocation optimisée en tenant compte de ces analyses.
106
+ """
107
+ )
108
+ print(f" Recommandation d'optimisation:\n {optimization_result.output[:400]}...\n")
109
+
110
+ # Résumé final
111
+ print("✅ Workflow terminé avec succès!")
112
+ print(f" - Analyse de risque: Complétée")
113
+ print(f" - Conseils fiscaux: Fournis")
114
+ print(f" - Optimisation: Recommandation générée")
115
+
116
+
117
+ async def exemple_agent_simple():
118
+ """Exemple simplifié d'un agent qui fait tout en une étape."""
119
+ print("\n🚀 Agent 3 (Variante): Agent tout-en-un")
120
+ print("=" * 60)
121
+
122
+ multi_agent = Agent(
123
+ finance_model,
124
+ model_settings=ModelSettings(max_output_tokens=2000), # Complete analysis needs more tokens
125
+ system_prompt=(
126
+ "Vous êtes un conseiller financier complet. "
127
+ "Pour chaque demande d'analyse, fournissez:\n"
128
+ "1. Une évaluation du risque (1-5)\n"
129
+ "2. Les implications fiscales en France\n"
130
+ "3. Une recommandation d'optimisation\n"
131
+ "Répondez toujours en français de manière structurée."
132
+ ),
133
+ )
134
+
135
+ question = (
136
+ "J'ai 50 000€ à investir avec un horizon de 15 ans. "
137
+ "Je pense à 60% actions, 30% obligations, 10% immobilier. "
138
+ "Analysez cette stratégie."
139
+ )
140
+
141
+ result = await multi_agent.run(question)
142
+ print(f"Question: {question}\n")
143
+ print(f"Analyse complète:\n{result.output[:500]}...")
144
+
145
+
146
+ if __name__ == "__main__":
147
+ print("Exécution du workflow multi-étapes...")
148
+ asyncio.run(workflow_analyse_investissement())
149
+
150
+ print("\n\n" + "=" * 60)
151
+ asyncio.run(exemple_agent_simple())
152
+
examples/agent_swift.py ADDED
@@ -0,0 +1,540 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agent SWIFT: Génération et parsing de messages SWIFT structurés
3
+
4
+ Cet agent démontre l'utilisation de PydanticAI pour:
5
+ - Générer des messages SWIFT formatés depuis du texte naturel
6
+ - Extraire les données structurées d'un message SWIFT
7
+ - Valider la structure des messages SWIFT
8
+ """
9
+
10
+ import asyncio
11
+ import re
12
+ from typing import Optional
13
+ from pydantic import BaseModel, Field, field_validator
14
+ from pydantic_ai import Agent, ModelSettings
15
+
16
+ from app.models import finance_model
17
+
18
+ # Imports relatifs pour les modules dans examples/
19
+ try:
20
+ from .swift_models import SWIFTMT103Structured, MT103Field32A
21
+ from .swift_extractor import (
22
+ parse_swift_mt103_advanced,
23
+ SwiftMT103Parsed,
24
+ format_swift_mt103_from_parsed,
25
+ )
26
+ except ImportError:
27
+ # Fallback pour exécution directe
28
+ import sys
29
+ from pathlib import Path
30
+ sys.path.insert(0, str(Path(__file__).parent))
31
+ from swift_models import SWIFTMT103Structured, MT103Field32A
32
+ from swift_extractor import (
33
+ parse_swift_mt103_advanced,
34
+ SwiftMT103Parsed,
35
+ format_swift_mt103_from_parsed,
36
+ )
37
+
38
+ # Model settings for SWIFT generation (complex structured output)
39
+ swift_model_settings = ModelSettings(
40
+ max_output_tokens=2000, # Increased for SWIFT message generation
41
+ )
42
+
43
+
44
+ # Modèle pour un message SWIFT MT103 (Transfert de fonds)
45
+ class SWIFTMT103(BaseModel):
46
+ """Message SWIFT MT103 - Transfert de fonds unique."""
47
+
48
+ # En-tête
49
+ message_type: str = Field(default="103", description="Type de message SWIFT (103)")
50
+ sender_bic: str = Field(description="BIC de la banque émettrice (8 ou 11 caractères)")
51
+ receiver_bic: str = Field(description="BIC de la banque réceptrice (8 ou 11 caractères)")
52
+
53
+ # Champs obligatoires
54
+ value_date: str = Field(description="Date de valeur au format YYYYMMDD")
55
+ currency: str = Field(description="Code devise ISO (3 lettres)", min_length=3, max_length=3)
56
+ amount: float = Field(description="Montant du transfert", gt=0)
57
+
58
+ # Champs optionnels
59
+ ordering_customer: str = Field(description="Données de l'ordre donneur (nom, adresse, compte)")
60
+ beneficiary: str = Field(description="Données du bénéficiaire (nom, adresse, compte)")
61
+ remittance_info: str | None = Field(default=None, description="Information pour le bénéficiaire")
62
+ charges: str = Field(default="OUR", description="Frais: OUR, SHA, BEN")
63
+ reference: str | None = Field(default=None, description="Référence du transfert")
64
+
65
+
66
+ class SWIFTMT940(BaseModel):
67
+ """Message SWIFT MT940 - Relevé bancaire."""
68
+
69
+ message_type: str = Field(default="940", description="Type de message SWIFT (940)")
70
+ account_identification: str = Field(description="Identification du compte (IBAN)")
71
+ statement_number: str = Field(description="Numéro de relevé")
72
+ opening_balance_date: str = Field(description="Date de solde d'ouverture YYYYMMDD")
73
+ opening_balance: float = Field(description="Solde d'ouverture")
74
+ opening_balance_indicator: str = Field(description="C (Crédit) ou D (Débit)")
75
+ currency: str = Field(description="Code devise ISO (3 lettres)")
76
+ transactions: list[dict[str, str | float]] = Field(description="Liste des transactions")
77
+
78
+
79
+ # Agent pour génération de messages SWIFT
80
+ swift_generator = Agent(
81
+ finance_model,
82
+ model_settings=swift_model_settings,
83
+ system_prompt=(
84
+ "Vous êtes un expert en messages SWIFT bancaires. "
85
+ "Votre rôle est de générer des messages SWIFT correctement formatés "
86
+ "à partir de descriptions en langage naturel. "
87
+ "Les messages SWIFT doivent être conformes aux standards internationaux. "
88
+ "Pour les montants, utilisez toujours le format numérique avec 2 décimales. "
89
+ "Les BIC doivent être valides (8 ou 11 caractères alphanumériques). "
90
+ "Répondez en français mais générez les messages SWIFT au format standard.\n\n"
91
+ "Vous disposez de 2000 tokens pour générer des messages SWIFT complets et détaillés."
92
+ ),
93
+ )
94
+
95
+
96
+ # Agent pour parsing de messages SWIFT avec extraction structurée
97
+ swift_parser = Agent(
98
+ finance_model,
99
+ model_settings=ModelSettings(max_output_tokens=2000),
100
+ system_prompt=(
101
+ "Vous êtes un expert en parsing de messages SWIFT bancaires. "
102
+ "Votre rôle est d'extraire précisément toutes les informations "
103
+ "à partir de messages SWIFT formatés (MT103, MT940, etc.).\n\n"
104
+ "Instructions importantes:\n"
105
+ "- Identifiez TOUS les champs SWIFT présents (même optionnels)\n"
106
+ "- Pour le champ :32A:, extrayez séparément la date (YYYYMMDD), devise (3 lettres), et montant\n"
107
+ "- Pour les champs :50K: et :59:, conservez toutes les lignes (nom, adresse, compte)\n"
108
+ "- Les dates doivent être au format YYYYMMDD\n"
109
+ "- Les montants doivent être numériques avec décimales\n"
110
+ "- Les BIC doivent être extraits des champs :52A:, :56A:, etc. si présents\n"
111
+ "- Répondez en JSON structuré pour faciliter le parsing"
112
+ ),
113
+ )
114
+
115
+
116
+ def format_swift_mt103(mt103: SWIFTMT103) -> str:
117
+ """Formate un message SWIFT MT103 selon les standards."""
118
+ lines = []
119
+
120
+ # En-tête SWIFT
121
+ lines.append(f":20:{mt103.reference or 'NONREF'}")
122
+ lines.append(f":23B:CRED")
123
+ lines.append(f":32A:{mt103.value_date}{mt103.currency}{mt103.amount:.2f}")
124
+ lines.append(f":50K:/{mt103.ordering_customer}")
125
+ lines.append(f":59:/{mt103.beneficiary}")
126
+
127
+ if mt103.remittance_info:
128
+ lines.append(f":70:{mt103.remittance_info}")
129
+
130
+ lines.append(f":71A:{mt103.charges}")
131
+
132
+ return "\n".join(lines)
133
+
134
+
135
+ class SWIFTExtractedMT103(BaseModel):
136
+ """Structure extraite d'un message SWIFT MT103."""
137
+
138
+ # Champ :20: - Référence du transfert
139
+ reference: str = Field(description="Référence du transfert (:20:)")
140
+
141
+ # Champ :23B: - Code instruction
142
+ instruction_code: str = Field(default="CRED", description="Code instruction (:23B:)")
143
+
144
+ # Champ :32A: - Date de valeur, devise, montant
145
+ value_date: str = Field(description="Date de valeur YYYYMMDD")
146
+ currency: str = Field(description="Code devise ISO 3 lettres")
147
+ amount: float = Field(description="Montant", gt=0)
148
+
149
+ # Champ :50K: ou :50A: - Ordre donneur (peut être multi-lignes)
150
+ ordering_customer: str = Field(description="Données ordonnateur (:50K: ou :50A:)")
151
+ ordering_customer_account: Optional[str] = Field(default=None, description="Compte ordonnateur (IBAN)")
152
+
153
+ # Champ :52A:, :52D: - Banque ordonnateur (optionnel)
154
+ ordering_bank_bic: Optional[str] = Field(default=None, description="BIC banque ordonnateur (:52A:)")
155
+ ordering_bank_name: Optional[str] = Field(default=None, description="Nom banque ordonnateur (:52D:)")
156
+
157
+ # Champ :56A:, :56D: - Banque intermédiaire (optionnel)
158
+ intermediary_bank_bic: Optional[str] = Field(default=None, description="BIC banque intermédiaire (:56A:)")
159
+ intermediary_bank_name: Optional[str] = Field(default=None, description="Nom banque intermédiaire (:56D:)")
160
+
161
+ # Champ :57A:, :57D: - Banque bénéficiaire (optionnel)
162
+ beneficiary_bank_bic: Optional[str] = Field(default=None, description="BIC banque bénéficiaire (:57A:)")
163
+ beneficiary_bank_name: Optional[str] = Field(default=None, description="Nom banque bénéficiaire (:57D:)")
164
+
165
+ # Champ :59: ou :59A: - Bénéficiaire (peut être multi-lignes)
166
+ beneficiary: str = Field(description="Données bénéficiaire (:59: ou :59A:)")
167
+ beneficiary_account: Optional[str] = Field(default=None, description="Compte bénéficiaire (IBAN)")
168
+
169
+ # Champ :70: - Information pour le bénéficiaire (optionnel)
170
+ remittance_info: Optional[str] = Field(default=None, description="Information bénéficiaire (:70:)")
171
+
172
+ # Champ :71A: - Frais
173
+ charges: str = Field(default="OUR", description="Frais: OUR/SHA/BEN (:71A:)")
174
+
175
+ # Champ :72: - Information pour la banque (optionnel)
176
+ bank_to_bank_info: Optional[str] = Field(default=None, description="Info banque à banque (:72:)")
177
+
178
+ @field_validator("value_date")
179
+ def validate_date(cls, v):
180
+ if len(v) != 8 or not v.isdigit():
181
+ raise ValueError(f"Date must be YYYYMMDD format, got: {v}")
182
+ return v
183
+
184
+ @field_validator("currency")
185
+ def validate_currency(cls, v):
186
+ if len(v) != 3 or not v.isalpha():
187
+ raise ValueError(f"Currency must be 3 letter ISO code, got: {v}")
188
+ return v.upper()
189
+
190
+ @field_validator("charges")
191
+ def validate_charges(cls, v):
192
+ valid = ["OUR", "SHA", "BEN"]
193
+ if v not in valid:
194
+ raise ValueError(f"Charges must be one of {valid}, got: {v}")
195
+ return v
196
+
197
+
198
+ def parse_swift_mt103(swift_text: str) -> SWIFTExtractedMT103:
199
+ """
200
+ Parse un message SWIFT MT103 et extrait tous les champs avec validation.
201
+
202
+ Gère:
203
+ - Champs multi-lignes (:50K:, :59:, etc.)
204
+ - Champs optionnels
205
+ - Extraction des BIC et noms de banques
206
+ - Validation des formats (dates, devises, montants)
207
+ """
208
+ # Nettoyer le texte
209
+ lines = [line.strip() for line in swift_text.strip().split("\n") if line.strip()]
210
+
211
+ parsed_data = {
212
+ "reference": "NONREF",
213
+ "instruction_code": "CRED",
214
+ "charges": "OUR",
215
+ }
216
+
217
+ i = 0
218
+ while i < len(lines):
219
+ line = lines[i]
220
+
221
+ # Champ :20: - Référence
222
+ if line.startswith(":20:"):
223
+ parsed_data["reference"] = line[4:].strip()
224
+
225
+ # Champ :23B: - Code instruction
226
+ elif line.startswith(":23B:"):
227
+ parsed_data["instruction_code"] = line[5:].strip()
228
+
229
+ # Champ :32A: - Date, devise, montant (format: YYYYMMDD + 3 lettres + montant)
230
+ elif line.startswith(":32A:"):
231
+ value = line[5:].strip()
232
+ if len(value) >= 11:
233
+ parsed_data["value_date"] = value[:8]
234
+ parsed_data["currency"] = value[8:11].upper()
235
+ try:
236
+ parsed_data["amount"] = float(value[11:].replace(",", "."))
237
+ except ValueError:
238
+ raise ValueError(f"Invalid amount format in :32A: {value[11:]}")
239
+
240
+ # Champ :50K:, :50A:, :50F: - Ordre donneur (peut être multi-lignes)
241
+ elif line.startswith(":50") and ":" in line:
242
+ tag_end = line.index(":")
243
+ tag = line[:tag_end+1]
244
+ content_parts = [line[tag_end+1:].strip()]
245
+ i += 1
246
+
247
+ # Lire les lignes suivantes jusqu'au prochain tag
248
+ while i < len(lines) and not lines[i].startswith(":"):
249
+ if lines[i].strip():
250
+ content_parts.append(lines[i].strip())
251
+ i += 1
252
+ i -= 1 # Revenir en arrière car on a avancé trop loin
253
+
254
+ full_content = "\n".join(content_parts)
255
+ parsed_data["ordering_customer"] = full_content
256
+
257
+ # Extraire le compte (IBAN) si présent
258
+ iban_match = re.search(r'([A-Z]{2}\d{2}[A-Z0-9\s]{12,34})', full_content)
259
+ if iban_match:
260
+ parsed_data["ordering_customer_account"] = iban_match.group(1).replace(" ", "")
261
+
262
+ # Champ :52A:, :52D: - Banque ordonnateur
263
+ elif line.startswith(":52A:"):
264
+ parsed_data["ordering_bank_bic"] = line[5:].strip()[:11]
265
+ elif line.startswith(":52D:"):
266
+ parsed_data["ordering_bank_name"] = line[5:].strip()
267
+
268
+ # Champ :56A:, :56D: - Banque intermédiaire
269
+ elif line.startswith(":56A:"):
270
+ parsed_data["intermediary_bank_bic"] = line[5:].strip()[:11]
271
+ elif line.startswith(":56D:"):
272
+ parsed_data["intermediary_bank_name"] = line[5:].strip()
273
+
274
+ # Champ :57A:, :57D: - Banque bénéficiaire
275
+ elif line.startswith(":57A:"):
276
+ parsed_data["beneficiary_bank_bic"] = line[5:].strip()[:11]
277
+ elif line.startswith(":57D:"):
278
+ parsed_data["beneficiary_bank_name"] = line[5:].strip()
279
+
280
+ # Champ :59:, :59A: - Bénéficiaire (peut être multi-lignes)
281
+ elif line.startswith(":59"):
282
+ tag_end = line.index(":")
283
+ tag = line[:tag_end+1]
284
+ content_parts = [line[tag_end+1:].strip()]
285
+ i += 1
286
+
287
+ # Lire les lignes suivantes jusqu'au prochain tag
288
+ while i < len(lines) and not lines[i].startswith(":"):
289
+ if lines[i].strip():
290
+ content_parts.append(lines[i].strip())
291
+ i += 1
292
+ i -= 1
293
+
294
+ full_content = "\n".join(content_parts)
295
+ parsed_data["beneficiary"] = full_content
296
+
297
+ # Extraire le compte (IBAN) si présent
298
+ iban_match = re.search(r'([A-Z]{2}\d{2}[A-Z0-9\s]{12,34})', full_content)
299
+ if iban_match:
300
+ parsed_data["beneficiary_account"] = iban_match.group(1).replace(" ", "")
301
+
302
+ # Champ :70: - Information pour bénéficiaire
303
+ elif line.startswith(":70:"):
304
+ content_parts = [line[4:].strip()]
305
+ i += 1
306
+ while i < len(lines) and not lines[i].startswith(":"):
307
+ if lines[i].strip():
308
+ content_parts.append(lines[i].strip())
309
+ i += 1
310
+ i -= 1
311
+ parsed_data["remittance_info"] = "\n".join(content_parts)
312
+
313
+ # Champ :71A: - Frais
314
+ elif line.startswith(":71A:"):
315
+ parsed_data["charges"] = line[5:].strip()
316
+
317
+ # Champ :72: - Information banque à banque
318
+ elif line.startswith(":72:"):
319
+ content_parts = [line[4:].strip()]
320
+ i += 1
321
+ while i < len(lines) and not lines[i].startswith(":"):
322
+ if lines[i].strip():
323
+ content_parts.append(lines[i].strip())
324
+ i += 1
325
+ i -= 1
326
+ parsed_data["bank_to_bank_info"] = "\n".join(content_parts)
327
+
328
+ i += 1
329
+
330
+ # Valider que les champs obligatoires sont présents
331
+ required_fields = ["value_date", "currency", "amount", "ordering_customer", "beneficiary"]
332
+ missing = [f for f in required_fields if f not in parsed_data]
333
+ if missing:
334
+ raise ValueError(f"Missing required fields: {missing}")
335
+
336
+ return SWIFTExtractedMT103(**parsed_data)
337
+
338
+
339
+ async def exemple_generation_swift():
340
+ """Exemple de génération d'un message SWIFT MT103."""
341
+ print("📨 Agent SWIFT: Génération de message MT103")
342
+ print("=" * 60)
343
+
344
+ demande = """
345
+ Je veux transférer 15 000 euros de mon compte à la BNP Paribas (BIC: BNPAFRPPXXX)
346
+ vers le compte de Jean Dupont à la Société Générale (BIC: SOGEFRPPXXX)
347
+ le 15 décembre 2024.
348
+
349
+ Mon compte: FR76 3000 4000 0100 0000 0000 123
350
+ Compte bénéficiaire: FR14 2004 1010 0505 0001 3M02 606
351
+ Référence: INVOICE-2024-001
352
+ Motif: Paiement facture décembre 2024
353
+ Les frais sont à ma charge.
354
+ """
355
+
356
+ print(f"Demande:\n{demande}\n")
357
+
358
+ prompt = f"""
359
+ Génère un message SWIFT MT103 à partir de cette demande:
360
+ {demande}
361
+
362
+ Fournis les informations structurées suivantes:
363
+ - BIC émetteur et récepteur
364
+ - Date de valeur (format YYYYMMDD)
365
+ - Devise et montant
366
+ - Données ordonnateur et bénéficiaire
367
+ - Référence et motif
368
+ - Qui paie les frais (OUR = ordonnateur, SHA = partagé, BEN = bénéficiaire)
369
+ """
370
+
371
+ result = await swift_generator.run(prompt)
372
+
373
+ print("✅ Message SWIFT généré:")
374
+ print(result.output)
375
+ print()
376
+
377
+ # Extraire les données structurées depuis la réponse avec validation
378
+ print("📊 Extraction des données structurées...")
379
+
380
+ # D'abord, extraire le message SWIFT brut (sans les explications)
381
+ swift_lines = []
382
+ for line in result.output.split("\n"):
383
+ if line.strip().startswith(":") and ":" in line:
384
+ swift_lines.append(line.strip())
385
+
386
+ if swift_lines:
387
+ swift_message = "\n".join(swift_lines)
388
+ print("Message SWIFT extrait:")
389
+ print(swift_message)
390
+ print()
391
+
392
+ # Parser avec validation Pydantic avancée
393
+ try:
394
+ extracted = parse_swift_mt103_advanced(swift_message)
395
+ print("✅ Données extraites et validées:")
396
+ print(f" Référence: {extracted.field_20}")
397
+ print(f" Date: {extracted.field_32A.value_date}")
398
+ print(f" Montant: {extracted.field_32A.amount:,.2f} {extracted.field_32A.currency}")
399
+ print(f" Ordonnateur: {extracted.field_50K[:50]}...")
400
+ print(f" Bénéficiaire: {extracted.field_59[:50]}...")
401
+ print(f" Frais: {extracted.field_71A}")
402
+ except Exception as e:
403
+ print(f"⚠️ Erreur de parsing structuré: {e}")
404
+ # Fallback: extraction via LLM
405
+ extraction = await swift_parser.run(
406
+ f"Extrais les données structurées du message SWIFT suivant:\n{swift_message}"
407
+ )
408
+ print(extraction.output[:500])
409
+ else:
410
+ # Fallback si aucun format SWIFT détecté
411
+ extraction = await swift_parser.run(
412
+ f"Extrais les données structurées du message SWIFT suivant:\n{result.output}"
413
+ )
414
+ print(extraction.output[:500])
415
+
416
+
417
+ async def exemple_parsing_swift():
418
+ """Exemple de parsing d'un message SWIFT existant."""
419
+ print("\n🔍 Agent SWIFT: Parsing de message MT103")
420
+ print("=" * 60)
421
+
422
+ swift_message = """
423
+ :20:NONREF
424
+ :23B:CRED
425
+ :32A:241215EUR15000.00
426
+ :50K:/FR76300040000100000000000123
427
+ ORDRE DUPONT JEAN
428
+ RUE DE LA REPUBLIQUE 123
429
+ 75001 PARIS FRANCE
430
+
431
+ :59:/FR1420041010050500013M02606
432
+ BENEFICIAIRE MARTIN PIERRE
433
+ AVENUE DES CHAMPS ELYSEES 456
434
+ 75008 PARIS FRANCE
435
+
436
+ :70:Paiement facture décembre 2024
437
+ :71A:OUR
438
+ """
439
+
440
+ print("Message SWIFT à parser:\n")
441
+ print(swift_message)
442
+ print()
443
+
444
+ result = await swift_parser.run(
445
+ f"Parse ce message SWIFT MT103 et extrais toutes les informations:\n{swift_message}\n\n"
446
+ "Fournis:\n- Type de message\n- Date de valeur\n- Montant et devise\n"
447
+ "- Données ordonnateur\n- Données bénéficiaire\n- Référence et motif\n- Frais"
448
+ )
449
+
450
+ print("✅ Données extraites:")
451
+ print(result.output)
452
+
453
+ # Parser technique avec validation Pydantic avancée
454
+ print("\n🔧 Parsing technique avec validation avancée:")
455
+ try:
456
+ # Utiliser le parser avancé
457
+ parsed = parse_swift_mt103_advanced(swift_message)
458
+ print("✅ Message SWIFT parsé et validé avec succès:")
459
+ print(f" Référence (:20:): {parsed.field_20}")
460
+ print(f" Code instruction (:23B:): {parsed.field_23B}")
461
+ print(f" Date de valeur: {parsed.field_32A.value_date}")
462
+ print(f" Devise: {parsed.field_32A.currency}")
463
+ print(f" Montant: {parsed.field_32A.amount:,.2f} {parsed.field_32A.currency}")
464
+ print(f" Ordonnateur (:50K:):\n {parsed.field_50K.replace(chr(10), chr(10) + ' ')}")
465
+ if parsed.ordering_customer_account:
466
+ print(f" → IBAN ordonnateur extrait: {parsed.ordering_customer_account}")
467
+ if parsed.field_52A:
468
+ print(f" Banque ordonnateur (:52A:): {parsed.field_52A}")
469
+ if parsed.field_56A:
470
+ print(f" Banque intermédiaire (:56A:): {parsed.field_56A}")
471
+ if parsed.field_57A:
472
+ print(f" Banque bénéficiaire (:57A:): {parsed.field_57A}")
473
+ print(f" Bénéficiaire (:59:):\n {parsed.field_59.replace(chr(10), chr(10) + ' ')}")
474
+ if parsed.beneficiary_account:
475
+ print(f" → IBAN bénéficiaire extrait: {parsed.beneficiary_account}")
476
+ if parsed.field_70:
477
+ print(f" Motif (:70:): {parsed.field_70}")
478
+ print(f" Frais (:71A:): {parsed.field_71A}")
479
+ if parsed.field_72:
480
+ print(f" Info banque (:72:): {parsed.field_72}")
481
+ except Exception as e:
482
+ print(f"❌ Erreur lors du parsing: {e}")
483
+ import traceback
484
+ traceback.print_exc()
485
+
486
+
487
+ async def exemple_synthese_swift():
488
+ """Exemple de synthèse d'un message SWIFT depuis plusieurs sources."""
489
+ print("\n🔄 Agent SWIFT: Synthèse de message")
490
+ print("=" * 60)
491
+
492
+ contexte = """
493
+ Informations de la transaction:
494
+ - Virement international de 50 000 USD
495
+ - De: ABC Bank New York (BIC: ABCDUS33XXX) vers XYZ Bank Paris (BIC: XYZDFRPPXXX)
496
+ - Date: 20 janvier 2025
497
+ - Compte ordonnateur: US64 SVBKUS6SXXX 123456789
498
+ - Compte bénéficiaire: FR76 3000 4000 0100 0000 0000 456
499
+ - Référence client: TXN-2025-001
500
+ - Motif: Paiement services consultance Q1 2025
501
+ - Frais partagés (SHA)
502
+ """
503
+
504
+ print(f"Contexte:\n{contexte}\n")
505
+
506
+ result = await swift_generator.run(
507
+ f"Génère un message SWIFT MT103 complet et correctement formaté:\n{contexte}\n\n"
508
+ "Assure-toi que:\n- Les BIC sont au bon format\n- La date est au format YYYYMMDD\n"
509
+ "- Le montant a 2 décimales\n- Les comptes incluent le code pays\n"
510
+ "- Tous les champs obligatoires sont présents"
511
+ )
512
+
513
+ print("✅ Message SWIFT synthétisé:")
514
+ swift_msg = result.output
515
+
516
+ # Extraire juste le format SWIFT si l'agent a ajouté des explications
517
+ swift_lines = []
518
+ for line in swift_msg.split("\n"):
519
+ if line.strip().startswith(":"):
520
+ swift_lines.append(line.strip())
521
+
522
+ if swift_lines:
523
+ print("\n".join(swift_lines))
524
+ else:
525
+ print(swift_msg)
526
+
527
+
528
+ if __name__ == "__main__":
529
+ print("\n" + "=" * 60)
530
+ print("EXEMPLES D'AGENTS SWIFT AVEC PYDANTICAI")
531
+ print("=" * 60 + "\n")
532
+
533
+ asyncio.run(exemple_generation_swift())
534
+ asyncio.run(exemple_parsing_swift())
535
+ asyncio.run(exemple_synthese_swift())
536
+
537
+ print("\n" + "=" * 60)
538
+ print("✅ Tous les exemples terminés!")
539
+ print("=" * 60)
540
+
examples/agent_with_tools_and_memory.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agent avec outils financiers et mémoire (history)
3
+
4
+ Cet exemple démontre:
5
+ 1. Utilisation d'outils Python pour calculs financiers
6
+ 2. Mémoire/conversation history pour maintenir le contexte
7
+ 3. Agents qui se souviennent des calculs précédents
8
+ """
9
+
10
+ import asyncio
11
+ from typing import Annotated, List
12
+ from pydantic import BaseModel
13
+ from pydantic_ai import Agent, ModelSettings
14
+
15
+ from app.models import finance_model
16
+
17
+
18
+ # Simple History wrapper for managing conversation
19
+ class ConversationHistory:
20
+ """Gère l'historique de conversation pour les agents."""
21
+
22
+ def __init__(self):
23
+ self.messages: List[dict] = []
24
+
25
+ def add_user_message(self, content: str):
26
+ """Ajoute un message utilisateur."""
27
+ # Pour simplifier, on crée une structure simple
28
+ # En production, utiliser les types corrects de PydanticAI
29
+ self.messages.append({"role": "user", "content": content})
30
+
31
+ def add_assistant_message(self, content: str):
32
+ """Ajoute un message assistant."""
33
+ self.messages.append({"role": "assistant", "content": content})
34
+
35
+ def get_history_for_agent(self) -> List[dict]:
36
+ """Retourne l'historique au format pour l'agent."""
37
+ return self.messages
38
+
39
+ def __len__(self):
40
+ return len(self.messages)
41
+
42
+ # ============================================================================
43
+ # OUTILS FINANCIERS
44
+ # ============================================================================
45
+
46
+ def calculer_valeur_future(
47
+ capital_initial: float,
48
+ taux_annuel: float,
49
+ duree_annees: float
50
+ ) -> str:
51
+ """Calcule la valeur future avec intérêts composés.
52
+
53
+ Args:
54
+ capital_initial: Montant initial en euros
55
+ taux_annuel: Taux d'intérêt annuel (ex: 0.04 pour 4%)
56
+ duree_annees: Durée en années
57
+
58
+ Returns:
59
+ Résultat formaté du calcul
60
+ """
61
+ valeur_future = capital_initial * (1 + taux_annuel) ** duree_annees
62
+ interets = valeur_future - capital_initial
63
+ rendement_pct = (interets / capital_initial) * 100
64
+
65
+ return (
66
+ f"💰 Valeur future: {valeur_future:,.2f}€\n"
67
+ f" Capital initial: {capital_initial:,.2f}€\n"
68
+ f" Intérêts générés: {interets:,.2f}€ ({rendement_pct:.2f}%)\n"
69
+ f" Durée: {duree_annees} ans à {taux_annuel*100:.2f}% par an"
70
+ )
71
+
72
+
73
+ def calculer_versement_mensuel(
74
+ capital_emprunte: float,
75
+ taux_annuel: float,
76
+ duree_annees: int
77
+ ) -> str:
78
+ """Calcule le versement mensuel pour un prêt immobilier.
79
+
80
+ Args:
81
+ capital_emprunte: Montant emprunté en euros
82
+ taux_annuel: Taux d'intérêt annuel (ex: 0.035 pour 3.5%)
83
+ duree_annees: Durée du prêt en années
84
+
85
+ Returns:
86
+ Résultat formaté du calcul
87
+ """
88
+ duree_mois = duree_annees * 12
89
+ taux_mensuel = taux_annuel / 12
90
+ versement = capital_emprunte * (
91
+ taux_mensuel * (1 + taux_mensuel) ** duree_mois
92
+ ) / ((1 + taux_mensuel) ** duree_mois - 1)
93
+
94
+ total_rembourse = versement * duree_mois
95
+ cout_total = total_rembourse - capital_emprunte
96
+
97
+ return (
98
+ f"🏠 Versement mensuel: {versement:,.2f}€\n"
99
+ f" Capital emprunté: {capital_emprunte:,.2f}€\n"
100
+ f" Total remboursé: {total_rembourse:,.2f}€\n"
101
+ f" Coût du crédit: {cout_total:,.2f}€\n"
102
+ f" Durée: {duree_annees} ans ({duree_mois} mois) à {taux_annuel*100:.2f}%"
103
+ )
104
+
105
+
106
+ def calculer_performance_portfolio(
107
+ valeur_initiale: float,
108
+ valeur_actuelle: float,
109
+ duree_jours: int
110
+ ) -> str:
111
+ """Calcule la performance d'un portfolio.
112
+
113
+ Args:
114
+ valeur_initiale: Valeur initiale en euros
115
+ valeur_actuelle: Valeur actuelle en euros
116
+ duree_jours: Durée en jours
117
+
118
+ Returns:
119
+ Résultat formaté du calcul
120
+ """
121
+ gain_absolu = valeur_actuelle - valeur_initiale
122
+ gain_pourcentage = (gain_absolu / valeur_initiale) * 100
123
+ rendement_annuelise = ((valeur_actuelle / valeur_initiale) ** (365 / duree_jours) - 1) * 100
124
+
125
+ return (
126
+ f"📈 Performance portfolio:\n"
127
+ f" Gain absolu: {gain_absolu:+,.2f}€ ({gain_pourcentage:+.2f}%)\n"
128
+ f" Rendement annualisé: {rendement_annuelise:+.2f}%\n"
129
+ f" Durée: {duree_jours} jours"
130
+ )
131
+
132
+
133
+ def calculer_ratio_dette(
134
+ dette_totale: float,
135
+ revenus_annuels: float
136
+ ) -> str:
137
+ """Calcule le ratio d'endettement.
138
+
139
+ Args:
140
+ dette_totale: Dette totale en euros
141
+ revenus_annuels: Revenus annuels en euros
142
+
143
+ Returns:
144
+ Résultat formaté du calcul
145
+ """
146
+ ratio = (dette_totale / revenus_annuels) * 100
147
+ annees_remboursement = dette_totale / revenus_annuels
148
+
149
+ return (
150
+ f"💳 Ratio d'endettement:\n"
151
+ f" Ratio: {ratio:.2f}% des revenus annuels\n"
152
+ f" Dette totale: {dette_totale:,.2f}€\n"
153
+ f" Revenus annuels: {revenus_annuels:,.2f}€\n"
154
+ f" Années de remboursement: {annees_remboursement:.2f} ans"
155
+ )
156
+
157
+
158
+ # ============================================================================
159
+ # AGENT AVEC OUTILS ET MÉMOIRE
160
+ # ============================================================================
161
+
162
+ finance_advisor = Agent(
163
+ finance_model,
164
+ model_settings=ModelSettings(max_output_tokens=2000),
165
+ system_prompt=(
166
+ "Vous êtes un conseiller financier expert qui aide les clients à prendre "
167
+ "des décisions financières éclairées. Vous avez accès à des outils de calcul "
168
+ "financier précis.\n\n"
169
+ "Utilisez les outils disponibles pour:\n"
170
+ "- Calculer les valeurs futures d'investissements\n"
171
+ "- Calculer les versements de prêts immobiliers\n"
172
+ "- Analyser la performance de portfolios\n"
173
+ "- Évaluer les ratios d'endettement\n\n"
174
+ "Gardez en mémoire les informations précédentes de la conversation pour "
175
+ "fournir des conseils cohérents et personnalisés.\n\n"
176
+ "Répondez toujours en français de manière claire et structurée."
177
+ ),
178
+ tools=[
179
+ calculer_valeur_future,
180
+ calculer_versement_mensuel,
181
+ calculer_performance_portfolio,
182
+ calculer_ratio_dette,
183
+ ],
184
+ )
185
+
186
+
187
+ # ============================================================================
188
+ # EXEMPLES D'UTILISATION
189
+ # ============================================================================
190
+
191
+ async def exemple_conversation_avec_memoire():
192
+ """Exemple de conversation avec mémoire (history)."""
193
+ print("💬 Exemple: Conversation avec mémoire et outils")
194
+ print("=" * 60)
195
+
196
+ # Créer une histoire de conversation vide
197
+ history = ConversationHistory()
198
+
199
+ # Question 1: Calcul initial
200
+ print("\n👤 Client: 'J'ai 50 000€ à placer à 4% par an pendant 10 ans. Combien aurai-je?'")
201
+ prompt1 = "J'ai 50 000€ à placer à 4% par an pendant 10 ans. Combien aurai-je?"
202
+ result1 = await finance_advisor.run(prompt1)
203
+ history.add_user_message(prompt1)
204
+ history.add_assistant_message(result1.output)
205
+ print(f"\n🤖 Conseiller:\n{result1.output[:400]}...")
206
+
207
+ # Question 2: Référence au calcul précédent (mémoire via contexte)
208
+ print("\n" + "-" * 60)
209
+ print("\n👤 Client: 'Et si j'augmente à 5%?'")
210
+ # Inclure le contexte précédent dans le prompt
211
+ context = "\n".join([
212
+ f"{'👤' if msg['role'] == 'user' else '🤖'} {msg['content'][:200]}..."
213
+ for msg in history.get_history_for_agent()
214
+ ])
215
+ prompt2 = f"Contexte précédent:\n{context}\n\nNouvelle question: Et si j'augmente le taux à 5%?"
216
+ result2 = await finance_advisor.run(prompt2)
217
+ history.add_user_message("Et si j'augmente le taux à 5%?")
218
+ history.add_assistant_message(result2.output)
219
+ print(f"\n🤖 Conseiller:\n{result2.output[:400]}...")
220
+
221
+ # Question 3: Nouvelle question avec contexte
222
+ print("\n" + "-" * 60)
223
+ print("\n👤 Client: 'En fait, je veux plutôt emprunter 200 000€ sur 20 ans à 3.5% pour un achat immobilier'")
224
+ context = "\n".join([
225
+ f"{msg['role']}: {msg['content'][:150]}..."
226
+ for msg in history.get_history_for_agent()[-4:] # Derniers 4 messages
227
+ ])
228
+ prompt3 = f"Contexte:\n{context}\n\nEn fait, je veux plutôt emprunter 200 000€ sur 20 ans à 3.5% pour un achat immobilier. Combien paierai-je par mois?"
229
+ result3 = await finance_advisor.run(prompt3)
230
+ history.add_user_message("En fait, je veux plutôt emprunter 200 000€ sur 20 ans à 3.5%")
231
+ history.add_assistant_message(result3.output)
232
+ print(f"\n🤖 Conseiller:\n{result3.output[:400]}...")
233
+
234
+ # Afficher l'historique complet
235
+ print("\n" + "=" * 60)
236
+ print("📚 Historique de la conversation:")
237
+ print("=" * 60)
238
+ for i, msg in enumerate(history.get_history_for_agent(), 1):
239
+ role = msg['role']
240
+ content = msg['content'][:100] + "..." if len(msg['content']) > 100 else msg['content']
241
+ print(f"{i}. {role.upper()}: {content}")
242
+
243
+
244
+ async def exemple_portfolio_avec_memoire():
245
+ """Exemple d'analyse de portfolio avec mémoire des calculs précédents."""
246
+ print("\n\n📊 Exemple: Analyse de portfolio avec mémoire")
247
+ print("=" * 60)
248
+
249
+ history = ConversationHistory()
250
+
251
+ # Initialisation du portfolio
252
+ print("\n👤 Client: 'Mon portfolio valait 100 000€ il y a 6 mois, aujourd'hui il vaut 115 000€'")
253
+ prompt1 = "Mon portfolio valait 100 000€ il y a 6 mois, aujourd'hui il vaut 115 000€. Calcule la performance."
254
+ result1 = await finance_advisor.run(prompt1)
255
+ history.add_user_message(prompt1)
256
+ history.add_assistant_message(result1.output)
257
+ print(f"\n🤖 Conseiller:\n{result1.output}")
258
+
259
+ # Suivi avec mémoire
260
+ print("\n" + "-" * 60)
261
+ print("\n👤 Client: 'Et si je projette cette performance sur 5 ans?'")
262
+ context = f"Contexte précédent:\n{result1.output[:300]}...\n\n"
263
+ prompt2 = context + "Et si je projette cette performance annuelle sur 5 ans avec mon capital actuel de 115 000€?"
264
+ result2 = await finance_advisor.run(prompt2)
265
+ history.add_user_message("Et si je projette cette performance sur 5 ans?")
266
+ history.add_assistant_message(result2.output)
267
+ print(f"\n🤖 Conseiller:\n{result2.output[:500]}...")
268
+
269
+ return history
270
+
271
+
272
+ async def exemple_analyse_complete_avec_memoire():
273
+ """Exemple complet d'analyse financière avec outils et mémoire."""
274
+ print("\n\n🎯 Exemple: Analyse financière complète avec mémoire")
275
+ print("=" * 60)
276
+
277
+ history = ConversationHistory()
278
+
279
+ questions = [
280
+ "Je gagne 80 000€ par an et j'ai une dette de 200 000€. Quel est mon ratio d'endettement?",
281
+ "Je veux emprunter 300 000€ pour une résidence principale à 3.5% sur 25 ans. Combien paierai-je?",
282
+ "Si j'investis les 74 000€ restants après le prêt à 5% par an pendant 15 ans, combien aurai-je?",
283
+ ]
284
+
285
+ for i, question in enumerate(questions, 1):
286
+ print(f"\n{'='*60}")
287
+ print(f"Question {i}: {question}")
288
+ print("=" * 60)
289
+
290
+ # Inclure le contexte si ce n'est pas la première question
291
+ if i > 1:
292
+ context = "\n".join([
293
+ f"{msg['role']}: {msg['content'][:200]}..."
294
+ for msg in history.get_history_for_agent()[-2:] # 2 derniers messages
295
+ ])
296
+ full_question = f"Contexte:\n{context}\n\n{question}"
297
+ else:
298
+ full_question = question
299
+
300
+ result = await finance_advisor.run(full_question)
301
+ history.add_user_message(question)
302
+ history.add_assistant_message(result.output)
303
+ print(f"\nRéponse:\n{result.output[:600]}...")
304
+
305
+ # Petit délai pour éviter les timeouts
306
+ await asyncio.sleep(1)
307
+
308
+ print("\n" + "=" * 60)
309
+ print("✅ Analyse complète terminée!")
310
+ print(f"📊 Total de messages dans l'historique: {len(history)}")
311
+
312
+
313
+ async def exemple_extraction_memoire():
314
+ """Montre comment extraire des informations de la mémoire."""
315
+ print("\n\n🔍 Exemple: Extraction d'informations de la mémoire")
316
+ print("=" * 60)
317
+
318
+ history = ConversationHistory()
319
+
320
+ # Conversation initiale
321
+ prompt1 = "J'ai un capital de 100 000€ à placer à 4% pendant 10 ans."
322
+ result1 = await finance_advisor.run(prompt1)
323
+ history.add_user_message(prompt1)
324
+ history.add_assistant_message(result1.output)
325
+
326
+ prompt2 = "Je gagne 75 000€ par an et j'ai une dette de 180 000€."
327
+ result2 = await finance_advisor.run(prompt2)
328
+ history.add_user_message(prompt2)
329
+ history.add_assistant_message(result2.output)
330
+
331
+ # Question qui utilise la mémoire
332
+ print("\n👤 Client: 'Résume ma situation financière'")
333
+ context = "\n".join([
334
+ f"{msg['role']}: {msg['content']}"
335
+ for msg in history.get_history_for_agent()
336
+ ])
337
+ result = await finance_advisor.run(
338
+ f"Contexte de la conversation:\n{context}\n\n"
339
+ "Peux-tu résumer ma situation financière actuelle basée sur ce que je t'ai dit?"
340
+ )
341
+
342
+ print(f"\n🤖 Conseiller:\n{result.output}")
343
+
344
+ # Afficher l'historique
345
+ print("\n" + "-" * 60)
346
+ print("📚 Messages dans l'historique:")
347
+ for msg in history.get_history_for_agent():
348
+ print(f" {msg['role']}: {msg['content'][:150]}...")
349
+
350
+
351
+ if __name__ == "__main__":
352
+ print("\n" + "=" * 60)
353
+ print("AGENTS AVEC OUTILS FINANCIERS ET MÉMOIRE")
354
+ print("=" * 60)
355
+
356
+ # Exemple 1: Conversation avec mémoire
357
+ asyncio.run(exemple_conversation_avec_memoire())
358
+
359
+ # Exemple 2: Portfolio avec mémoire
360
+ asyncio.run(exemple_portfolio_avec_memoire())
361
+
362
+ # Exemple 3: Extraction de mémoire
363
+ asyncio.run(exemple_extraction_memoire())
364
+
365
+ print("\n\n" + "=" * 60)
366
+ print("✅ Tous les exemples terminés!")
367
+ print("=" * 60)
368
+
examples/memory_strategies.py ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stratégies de gestion de mémoire pour agents financiers
3
+
4
+ Démontre différentes approches pour gérer la mémoire et l'historique
5
+ des conversations avec PydanticAI.
6
+ """
7
+
8
+ import asyncio
9
+ from typing import List
10
+ from pydantic_ai import Agent, ModelSettings
11
+
12
+ from app.models import finance_model
13
+
14
+
15
+ # Simple History wrapper
16
+ class ConversationHistory:
17
+ """Gère l'historique de conversation pour les agents."""
18
+
19
+ def __init__(self):
20
+ self.messages: List[dict] = []
21
+
22
+ def add_user_message(self, content: str):
23
+ """Ajoute un message utilisateur."""
24
+ self.messages.append({"role": "user", "content": content})
25
+
26
+ def add_assistant_message(self, content: str):
27
+ """Ajoute un message assistant."""
28
+ self.messages.append({"role": "assistant", "content": content})
29
+
30
+ def get_history_for_agent(self) -> List[dict]:
31
+ """Retourne l'historique au format pour l'agent."""
32
+ return self.messages
33
+
34
+ def all_messages(self):
35
+ """Itérateur sur tous les messages."""
36
+ return iter(self.messages)
37
+
38
+ def __len__(self):
39
+ return len(self.messages)
40
+
41
+
42
+ # ============================================================================
43
+ # AGENT FINANCIER DE BASE
44
+ # ============================================================================
45
+
46
+ finance_agent = Agent(
47
+ finance_model,
48
+ model_settings=ModelSettings(max_output_tokens=1500),
49
+ system_prompt=(
50
+ "Vous êtes un conseiller financier expert. "
51
+ "Vous gardez en mémoire les informations précédentes de la conversation "
52
+ "pour fournir des conseils cohérents et personnalisés. "
53
+ "Répondez toujours en français."
54
+ ),
55
+ )
56
+
57
+
58
+ # ============================================================================
59
+ # STRATÉGIE 1: MÉMOIRE SIMPLE (HISTORY)
60
+ # ============================================================================
61
+
62
+ async def strategie_memoire_simple():
63
+ """Mémoire basique avec History - tout est conservé."""
64
+ print("📝 Stratégie 1: Mémoire simple (tout est conservé)")
65
+ print("=" * 60)
66
+
67
+ history = ConversationHistory()
68
+
69
+ # Conversation
70
+ result1 = await finance_agent.run("J'ai 100 000€ à investir.")
71
+ history.add_user_message("J'ai 100 000€ à investir.")
72
+ history.add_assistant_message(result1.output)
73
+
74
+ result2 = await finance_agent.run("Mon objectif est la retraite dans 20 ans.")
75
+ history.add_user_message("Mon objectif est la retraite dans 20 ans.")
76
+ history.add_assistant_message(result2.output)
77
+
78
+ # Question qui nécessite la mémoire
79
+ context = "\n".join([f"{msg['role']}: {msg['content'][:200]}" for msg in history.get_history_for_agent()])
80
+ result = await finance_agent.run(
81
+ f"Contexte:\n{context}\n\nQuel type d'investissement me recommandes-tu?"
82
+ )
83
+
84
+ print(f"\nRéponse:\n{result.output[:400]}...")
85
+ print(f"\n📊 Messages dans l'historique: {len(history)}")
86
+
87
+
88
+ # ============================================================================
89
+ # STRATÉGIE 2: MÉMOIRE SÉLECTIVE (FILTRAGE)
90
+ # ============================================================================
91
+
92
+ class SelectiveMemory:
93
+ """Mémoire sélective qui ne garde que les informations importantes."""
94
+
95
+ def __init__(self):
96
+ self.history = History()
97
+ self.important_facts = []
98
+
99
+ def add_fact(self, fact: str):
100
+ """Ajoute un fait important à retenir."""
101
+ self.important_facts.append(fact)
102
+
103
+ def get_context(self) -> str:
104
+ """Retourne le contexte des faits importants."""
105
+ if not self.important_facts:
106
+ return ""
107
+ return "Faits importants à retenir:\n" + "\n".join(f"- {f}" for f in self.important_facts)
108
+
109
+
110
+ async def strategie_memoire_selective():
111
+ """Mémoire sélective - on garde seulement les faits clés."""
112
+ print("\n\n🎯 Stratégie 2: Mémoire sélective (faits clés)")
113
+ print("=" * 60)
114
+
115
+ memory = SelectiveMemory()
116
+ history = ConversationHistory()
117
+
118
+ # Conversation avec extraction de faits
119
+ prompt = "J'ai 100 000€ à investir pour la retraite dans 20 ans. J'ai 45 ans."
120
+ result1 = await finance_agent.run(prompt)
121
+ history.add_user_message(prompt)
122
+ history.add_assistant_message(result1.output)
123
+ memory.add_fact("Capital: 100 000€")
124
+ memory.add_fact("Objectif: Retraite")
125
+ memory.add_fact("Horizon: 20 ans")
126
+ memory.add_fact("Âge: 45 ans")
127
+
128
+ print(f"\n📌 Faits extraits: {memory.important_facts}")
129
+
130
+ # Nouvelle question avec contexte des faits
131
+ context = memory.get_context()
132
+ result2 = await finance_agent.run(
133
+ f"{context}\n\nQuestion: Quel type d'investissement me recommandes-tu?"
134
+ )
135
+
136
+ print(f"\nRéponse:\n{result2.output[:400]}...")
137
+
138
+
139
+ # ============================================================================
140
+ # STRATÉGIE 3: MÉMOIRE STRUCTURÉE (PROFIL CLIENT)
141
+ # ============================================================================
142
+
143
+ class ClientProfile:
144
+ """Profil structuré du client."""
145
+
146
+ def __init__(self):
147
+ self.age: int | None = None
148
+ self.revenus_annuels: float | None = None
149
+ self.capital: float | None = None
150
+ self.objectifs: list[str] = []
151
+ self.horizon: int | None = None
152
+ self.profil_risque: str | None = None
153
+
154
+ def to_context(self) -> str:
155
+ """Convertit le profil en contexte pour l'agent."""
156
+ parts = ["Profil client:"]
157
+ if self.age:
158
+ parts.append(f"- Âge: {self.age} ans")
159
+ if self.revenus_annuels:
160
+ parts.append(f"- Revenus annuels: {self.revenus_annuels:,.0f}€")
161
+ if self.capital:
162
+ parts.append(f"- Capital: {self.capital:,.0f}€")
163
+ if self.objectifs:
164
+ parts.append(f"- Objectifs: {', '.join(self.objectifs)}")
165
+ if self.horizon:
166
+ parts.append(f"- Horizon: {self.horizon} ans")
167
+ if self.profil_risque:
168
+ parts.append(f"- Profil de risque: {self.profil_risque}")
169
+ return "\n".join(parts)
170
+
171
+
172
+ async def strategie_memoire_structuree():
173
+ """Mémoire structurée avec profil client."""
174
+ print("\n\n📋 Stratégie 3: Mémoire structurée (profil client)")
175
+ print("=" * 60)
176
+
177
+ profile = ClientProfile()
178
+ history = ConversationHistory()
179
+
180
+ # Construction du profil
181
+ prompt = "J'ai 45 ans, je gagne 80 000€ par an et j'ai 150 000€ d'épargne. Je veux préparer ma retraite dans 20 ans avec un profil modéré."
182
+ result1 = await finance_agent.run(prompt)
183
+ history.add_user_message(prompt)
184
+ history.add_assistant_message(result1.output)
185
+
186
+ # Extraction structurée (ici simplifiée, idéalement avec output_type)
187
+ profile.age = 45
188
+ profile.revenus_annuels = 80000
189
+ profile.capital = 150000
190
+ profile.objectifs = ["Retraite"]
191
+ profile.horizon = 20
192
+ profile.profil_risque = "Modéré"
193
+
194
+ print(f"\n📋 Profil client construit:\n{profile.to_context()}")
195
+
196
+ # Utilisation du profil dans les conseils
197
+ context = profile.to_context()
198
+ result2 = await finance_agent.run(
199
+ f"{context}\n\nQuelle stratégie d'investissement me recommandes-tu?"
200
+ )
201
+
202
+ print(f"\nRéponse:\n{result2.output[:500]}...")
203
+
204
+
205
+ # ============================================================================
206
+ # STRATÉGIE 4: MÉMOIRE AVEC RÉSUMÉ (COMPRESSION)
207
+ # ============================================================================
208
+
209
+ async def strategie_memoire_avec_resume():
210
+ """Mémoire avec résumé périodique pour éviter la surcharge."""
211
+ print("\n\n📄 Stratégie 4: Mémoire avec résumé (compression)")
212
+ print("=" * 60)
213
+
214
+ history = ConversationHistory()
215
+
216
+ # Conversation longue
217
+ messages = [
218
+ "J'ai 45 ans et je gagne 80 000€ par an.",
219
+ "J'ai 150 000€ d'épargne actuellement.",
220
+ "Mon objectif est la retraite dans 20 ans.",
221
+ "J'ai un profil de risque modéré.",
222
+ "Je préfère les investissements diversifiés.",
223
+ ]
224
+
225
+ for msg in messages:
226
+ result = await finance_agent.run(msg)
227
+ history.add_user_message(msg)
228
+ history.add_assistant_message(result.output)
229
+ print(f" ✓ Ajouté: {msg}")
230
+
231
+ # Créer un résumé quand l'historique devient long
232
+ if len(history) > 6:
233
+ print("\n📝 Création d'un résumé de conversation...")
234
+ context = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history.get_history_for_agent()])
235
+ summary_result = await finance_agent.run(
236
+ f"Contexte:\n{context}\n\n"
237
+ "Résume en 3-4 phrases les informations clés que le client t'a données "
238
+ "dans cette conversation pour créer un profil client."
239
+ )
240
+ print(f"\n📄 Résumé:\n{summary_result.output[:300]}...")
241
+
242
+ # Utiliser le résumé comme nouveau contexte
243
+ summary_context = summary_result.output
244
+ result = await finance_agent.run(
245
+ f"Contexte client:\n{summary_context}\n\n"
246
+ "Quelle stratégie d'investissement recommandes-tu?"
247
+ )
248
+ print(f"\n💡 Recommandation basée sur le résumé:\n{result.output[:400]}...")
249
+
250
+
251
+ # ============================================================================
252
+ # STRATÉGIE 5: MÉMOIRE MULTI-SESSION (PERSISTANCE)
253
+ # ============================================================================
254
+
255
+ import json
256
+ from datetime import datetime
257
+
258
+
259
+ class PersistentMemory:
260
+ """Mémoire persistante qui peut être sauvegardée/chargée."""
261
+
262
+ def __init__(self, client_id: str):
263
+ self.client_id = client_id
264
+ self.history = History()
265
+ self.facts = {}
266
+ self.last_interaction = None
267
+
268
+ def save(self, filepath: str):
269
+ """Sauvegarde la mémoire dans un fichier."""
270
+ data = {
271
+ "client_id": self.client_id,
272
+ "facts": self.facts,
273
+ "last_interaction": self.last_interaction.isoformat() if self.last_interaction else None,
274
+ "messages": [
275
+ {"role": msg.role, "content": msg.content}
276
+ for msg in self.history.all_messages()
277
+ ],
278
+ }
279
+ with open(filepath, "w") as f:
280
+ json.dump(data, f, indent=2, ensure_ascii=False)
281
+
282
+ @classmethod
283
+ def load(cls, filepath: str):
284
+ """Charge la mémoire depuis un fichier."""
285
+ with open(filepath, "r") as f:
286
+ data = json.load(f)
287
+
288
+ memory = cls(data["client_id"])
289
+ memory.facts = data.get("facts", {})
290
+ if data.get("last_interaction"):
291
+ memory.last_interaction = datetime.fromisoformat(data["last_interaction"])
292
+
293
+ # Reconstruire l'historique (simplifié)
294
+ for msg_data in data.get("messages", []):
295
+ # Note: Cette reconstruction est simplifiée
296
+ # En production, utilisez l'API History correctement
297
+ pass
298
+
299
+ return memory
300
+
301
+
302
+ async def strategie_memoire_persistante():
303
+ """Mémoire persistante entre sessions."""
304
+ print("\n\n💾 Stratégie 5: Mémoire persistante (multi-session)")
305
+ print("=" * 60)
306
+
307
+ # Session 1
308
+ memory = PersistentMemory("client_001")
309
+ memory.facts = {
310
+ "age": 45,
311
+ "revenus": 80000,
312
+ "capital": 150000,
313
+ "objectif": "Retraite",
314
+ }
315
+ memory.last_interaction = datetime.now()
316
+
317
+ # Sauvegarder
318
+ filepath = "/tmp/client_memory.json"
319
+ memory.save(filepath)
320
+ print(f"✅ Mémoire sauvegardée: {filepath}")
321
+
322
+ # Simuler une nouvelle session (chargement)
323
+ print("\n🔄 Nouvelle session - Chargement de la mémoire...")
324
+ loaded_memory = PersistentMemory.load(filepath)
325
+
326
+ print(f"📋 Faits chargés: {loaded_memory.facts}")
327
+ print(f"🕐 Dernière interaction: {loaded_memory.last_interaction}")
328
+
329
+ # Utiliser la mémoire chargée
330
+ context = "Contexte client:\n" + "\n".join(
331
+ f"- {k}: {v}" for k, v in loaded_memory.facts.items()
332
+ )
333
+
334
+ result = await finance_agent.run(
335
+ f"{context}\n\nJe reviens vous voir 6 mois plus tard. Mon capital est maintenant de 160 000€. "
336
+ "Quelle est ma nouvelle situation?"
337
+ )
338
+
339
+ print(f"\nRéponse:\n{result.output[:400]}...")
340
+
341
+
342
+ if __name__ == "__main__":
343
+ print("\n" + "=" * 60)
344
+ print("STRATÉGIES DE GESTION DE MÉMOIRE POUR AGENTS")
345
+ print("=" * 60)
346
+
347
+ # Stratégie 1
348
+ asyncio.run(strategie_memoire_simple())
349
+
350
+ # Stratégie 2
351
+ asyncio.run(strategie_memoire_selective())
352
+
353
+ # Stratégie 3
354
+ asyncio.run(strategie_memoire_structuree())
355
+
356
+ # Stratégie 4
357
+ asyncio.run(strategie_memoire_avec_resume())
358
+
359
+ # Stratégie 5
360
+ asyncio.run(strategie_memoire_persistante())
361
+
362
+ print("\n\n" + "=" * 60)
363
+ print("✅ Toutes les stratégies démontrées!")
364
+ print("=" * 60)
365
+
examples/swift_extractor.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module d'extraction avancée de messages SWIFT avec validation Pydantic.
3
+
4
+ Fournit des fonctions robustes pour parser et valider les messages SWIFT,
5
+ avec support des champs multi-lignes et validation stricte des formats.
6
+ """
7
+
8
+ import re
9
+ from typing import Optional
10
+ from pydantic import BaseModel, Field, field_validator, ValidationError
11
+
12
+
13
+ class SwiftField32A(BaseModel):
14
+ """Représente le champ :32A: (Date de valeur, devise, montant)."""
15
+ value_date: str = Field(description="Date YYYYMMDD")
16
+ currency: str = Field(description="Code devise ISO 3 lettres")
17
+ amount: float = Field(description="Montant", gt=0)
18
+
19
+ @field_validator("value_date")
20
+ @classmethod
21
+ def validate_date(cls, v: str) -> str:
22
+ if len(v) != 8 or not v.isdigit():
23
+ raise ValueError(f"Date must be YYYYMMDD format, got: {v}")
24
+ # Valider que c'est une date valide
25
+ year = int(v[:4])
26
+ month = int(v[4:6])
27
+ day = int(v[6:8])
28
+ if not (1900 <= year <= 2100 and 1 <= month <= 12 and 1 <= day <= 31):
29
+ raise ValueError(f"Invalid date values: {v}")
30
+ return v
31
+
32
+ @field_validator("currency")
33
+ @classmethod
34
+ def validate_currency(cls, v: str) -> str:
35
+ if len(v) != 3 or not v.isalpha():
36
+ raise ValueError(f"Currency must be 3 letter ISO code, got: {v}")
37
+ return v.upper()
38
+
39
+
40
+ class SwiftMT103Parsed(BaseModel):
41
+ """Structure complète d'un message SWIFT MT103 parsé et validé."""
42
+
43
+ # Champs obligatoires
44
+ field_20: str = Field(description=":20: Référence du transfert")
45
+ field_32A: SwiftField32A = Field(description=":32A: Date, devise, montant")
46
+ field_50K: str = Field(description=":50K: Ordre donneur")
47
+ field_59: str = Field(description=":59: Bénéficiaire")
48
+
49
+ # Champs optionnels avec valeurs par défaut
50
+ field_23B: str = Field(default="CRED", description=":23B: Code instruction")
51
+ field_52A: Optional[str] = Field(default=None, description=":52A: BIC banque ordonnateur")
52
+ field_56A: Optional[str] = Field(default=None, description=":56A: BIC banque intermédiaire")
53
+ field_57A: Optional[str] = Field(default=None, description=":57A: BIC banque bénéficiaire")
54
+ field_70: Optional[str] = Field(default=None, description=":70: Information pour bénéficiaire")
55
+ field_71A: str = Field(default="OUR", description=":71A: Frais (OUR/SHA/BEN)")
56
+ field_72: Optional[str] = Field(default=None, description=":72: Information banque à banque")
57
+
58
+ # Champs extraits (IBAN, noms, etc.)
59
+ ordering_customer_account: Optional[str] = Field(default=None, description="IBAN ordonnateur extrait")
60
+ beneficiary_account: Optional[str] = Field(default=None, description="IBAN bénéficiaire extrait")
61
+
62
+ @field_validator("field_71A")
63
+ @classmethod
64
+ def validate_charges(cls, v: str) -> str:
65
+ valid = ["OUR", "SHA", "BEN"]
66
+ if v not in valid:
67
+ raise ValueError(f"Charges must be one of {valid}, got: {v}")
68
+ return v
69
+
70
+ @field_validator("field_52A", "field_56A", "field_57A")
71
+ @classmethod
72
+ def validate_bic(cls, v: Optional[str]) -> Optional[str]:
73
+ if v is None:
74
+ return v
75
+ v = v.strip()[:11] # BIC max 11 caractères
76
+ if len(v) not in [8, 11]:
77
+ raise ValueError(f"BIC must be 8 or 11 characters, got: {len(v)}")
78
+ return v
79
+
80
+
81
+ def extract_iban_from_text(text: str) -> Optional[str]:
82
+ """Extrait un IBAN depuis un texte (format: 2 lettres + 2 chiffres + 12-34 caractères)."""
83
+ # Pattern IBAN: 2 lettres pays + 2 chiffres + 12-34 alphanumériques
84
+ # Les IBAN ont une longueur fixe par pays, mais on accepte 15-34 caractères
85
+ pattern = r'([A-Z]{2}\d{2}[A-Z0-9\s]{12,30})'
86
+ matches = re.finditer(pattern, text)
87
+
88
+ for match in matches:
89
+ iban_candidate = match.group(1).replace(" ", "").replace("\n", "")
90
+
91
+ # Vérifier la longueur
92
+ if not (15 <= len(iban_candidate) <= 34):
93
+ continue
94
+
95
+ # Vérifier qu'on n'a pas capturé du texte après l'IBAN
96
+ # Les IBAN se terminent typiquement avant un mot (lettre minuscule après majuscules/chiffres)
97
+ start_pos = match.start()
98
+ end_pos = match.end()
99
+
100
+ # Si on commence par "/" ou après un "/", c'est probablement un IBAN
101
+ if start_pos > 0 and text[start_pos - 1] == "/":
102
+ # Couper au premier caractère non-alphanumérique ou après 34 caractères max
103
+ iban_clean = iban_candidate[:34] if len(iban_candidate) > 34 else iban_candidate
104
+ # Si on a capturé trop, chercher une coupure naturelle
105
+ if len(iban_clean) > 20: # La plupart des IBAN font 27 caractères
106
+ # Tronquer à une longueur raisonnable (IBAN max = 34)
107
+ iban_clean = iban_clean[:34]
108
+ return iban_clean
109
+
110
+ # Vérifier les caractères après la match
111
+ if end_pos < len(text):
112
+ next_char = text[end_pos]
113
+ # Si le caractère suivant est une lettre minuscule, on a probablement capturé trop
114
+ if next_char.islower():
115
+ continue
116
+
117
+ return iban_candidate[:34] if len(iban_candidate) > 34 else iban_candidate
118
+
119
+ return None
120
+
121
+
122
+ def extract_bic_from_text(text: str) -> Optional[str]:
123
+ """Extrait un BIC depuis un texte (8 ou 11 caractères alphanumériques)."""
124
+ # Pattern BIC: 4 lettres + 2 lettres + 2 caractères (optionnel: 3 caractères)
125
+ pattern = r'\b([A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?)\b'
126
+ matches = re.findall(pattern, text)
127
+ if matches:
128
+ return matches[0][0] # Retourner le BIC complet
129
+ return None
130
+
131
+
132
+ def parse_swift_field_32a(value: str) -> SwiftField32A:
133
+ """
134
+ Parse le champ :32A: (format: YYMMDD ou YYYYMMDD + 3 lettres + montant).
135
+
136
+ Formats supportés:
137
+ - YYMMDD + currency + amount (ex: 241215EUR15000.00)
138
+ - YYYYMMDD + currency + amount (ex: 20241215EUR15000.00)
139
+ """
140
+ value = value.strip()
141
+
142
+ # Déterminer si c'est un format à 6 chiffres (YYMMDD) ou 8 chiffres (YYYYMMDD)
143
+ # On cherche le début de la devise (3 lettres majuscules)
144
+ currency_match = re.search(r'([A-Z]{3})', value[6:]) # Chercher après les 6 premiers chiffres
145
+
146
+ if not currency_match:
147
+ raise ValueError(f"Cannot find currency code in :32A: {value}")
148
+
149
+ currency_start = currency_match.start() + 6 # Position de début de la devise
150
+ date_str = value[:currency_start]
151
+ currency_str = currency_match.group(1)
152
+ amount_str = value[currency_start + 3:].strip() # Ne pas remplacer les virgules ici
153
+
154
+ # Convertir YYMMDD en YYYYMMDD si nécessaire
155
+ if len(date_str) == 6:
156
+ # Format YYMMDD - convertir en YYYYMMDD
157
+ year = int(date_str[:2])
158
+ # Supposer années 2000-2099 si YY < 50, sinon 1900-1999
159
+ full_year = 2000 + year if year < 50 else 1900 + year
160
+ date_str = f"{full_year}{date_str[2:]}"
161
+ elif len(date_str) != 8:
162
+ raise ValueError(f"Date must be 6 (YYMMDD) or 8 (YYYYMMDD) digits, got: {date_str} (length {len(date_str)})")
163
+
164
+ if not amount_str:
165
+ raise ValueError(f"Missing amount in :32A: {value}")
166
+
167
+ # Gérer les formats de montants variés
168
+ # Format européen: 1.234,56 (point pour milliers, virgule pour décimales)
169
+ # Format anglais: 1,234.56 (virgule pour milliers, point pour décimales)
170
+ # Format simple: 1234.56 ou 1234,56
171
+
172
+ # Détecter le format
173
+ has_comma = "," in amount_str
174
+ has_dot = "." in amount_str
175
+
176
+ if has_comma and has_dot:
177
+ # Déterminer lequel est le séparateur de décimales
178
+ comma_pos = amount_str.rfind(",")
179
+ dot_pos = amount_str.rfind(".")
180
+
181
+ if comma_pos > dot_pos:
182
+ # Format européen: 1.234,56 → 1234.56
183
+ amount_str = amount_str.replace(".", "").replace(",", ".")
184
+ else:
185
+ # Format anglais: 1,234.56 → 1234.56
186
+ amount_str = amount_str.replace(",", "")
187
+ elif has_comma and not has_dot:
188
+ # Format européen sans milliers: 1234,56 → 1234.56
189
+ amount_str = amount_str.replace(",", ".")
190
+
191
+ try:
192
+ amount = float(amount_str)
193
+ except ValueError:
194
+ raise ValueError(f"Invalid amount format in :32A: {amount_str}")
195
+
196
+ return SwiftField32A(
197
+ value_date=date_str,
198
+ currency=currency_str,
199
+ amount=amount
200
+ )
201
+
202
+
203
+ def parse_swift_mt103_advanced(swift_text: str) -> SwiftMT103Parsed:
204
+ """
205
+ Parse un message SWIFT MT103 avec validation complète.
206
+
207
+ Gère:
208
+ - Tous les champs standard MT103
209
+ - Champs multi-lignes
210
+ - Extraction automatique d'IBAN et BIC
211
+ - Validation stricte avec Pydantic
212
+ """
213
+ lines = [line.rstrip() for line in swift_text.split("\n")]
214
+
215
+ data = {}
216
+ i = 0
217
+
218
+ while i < len(lines):
219
+ line = lines[i].strip()
220
+ if not line:
221
+ i += 1
222
+ continue
223
+
224
+ # Pattern pour identifier les tags SWIFT (format :XX: ou :XXA:, :XXB:, etc.)
225
+ tag_match = re.match(r'^:(\d{2}[A-Z]?):', line)
226
+ if not tag_match:
227
+ i += 1
228
+ continue
229
+
230
+ tag = tag_match.group(0) # e.g. ":20:", ":32A:"
231
+ tag_num = tag_match.group(1) # e.g. "20", "32A"
232
+ content_start = len(tag)
233
+
234
+ # Extraire le contenu (peut être multi-lignes)
235
+ content_lines = []
236
+ current_line = line[content_start:].strip()
237
+ if current_line:
238
+ content_lines.append(current_line)
239
+
240
+ # Lire les lignes suivantes jusqu'au prochain tag ou fin
241
+ i += 1
242
+ while i < len(lines):
243
+ next_line = lines[i].strip()
244
+ if next_line.startswith(":"):
245
+ break
246
+ if next_line:
247
+ content_lines.append(next_line)
248
+ i += 1
249
+
250
+ full_content = "\n".join(content_lines)
251
+
252
+ # Traitement selon le tag
253
+ if tag_num == "20":
254
+ data["field_20"] = full_content or "NONREF"
255
+
256
+ elif tag_num == "23B":
257
+ data["field_23B"] = full_content or "CRED"
258
+
259
+ elif tag_num == "32A":
260
+ data["field_32A"] = parse_swift_field_32a(full_content)
261
+
262
+ elif tag_num.startswith("50"):
263
+ data["field_50K"] = full_content
264
+ # Extraire IBAN si présent
265
+ iban = extract_iban_from_text(full_content)
266
+ if iban:
267
+ data["ordering_customer_account"] = iban
268
+
269
+ elif tag_num == "52A":
270
+ bic = extract_bic_from_text(full_content) or full_content[:11]
271
+ data["field_52A"] = bic
272
+
273
+ elif tag_num == "56A":
274
+ bic = extract_bic_from_text(full_content) or full_content[:11]
275
+ data["field_56A"] = bic
276
+
277
+ elif tag_num == "57A":
278
+ bic = extract_bic_from_text(full_content) or full_content[:11]
279
+ data["field_57A"] = bic
280
+
281
+ elif tag_num.startswith("59"):
282
+ data["field_59"] = full_content
283
+ # Extraire IBAN si présent
284
+ iban = extract_iban_from_text(full_content)
285
+ if iban:
286
+ data["beneficiary_account"] = iban
287
+
288
+ elif tag_num == "70":
289
+ data["field_70"] = full_content
290
+
291
+ elif tag_num == "71A":
292
+ data["field_71A"] = full_content.strip() or "OUR"
293
+
294
+ elif tag_num == "72":
295
+ data["field_72"] = full_content
296
+
297
+ # Ne pas incrémenter i ici car on l'a déjà fait dans la boucle while
298
+
299
+ # Validation avec Pydantic
300
+ try:
301
+ return SwiftMT103Parsed(**data)
302
+ except ValidationError as e:
303
+ raise ValueError(f"Validation error: {e}") from e
304
+
305
+
306
+ def format_swift_mt103_from_parsed(parsed: SwiftMT103Parsed) -> str:
307
+ """Reformate un message SWIFT MT103 depuis une structure parsée."""
308
+ lines = [
309
+ f":20:{parsed.field_20}",
310
+ f":23B:{parsed.field_23B}",
311
+ f":32A:{parsed.field_32A.value_date}{parsed.field_32A.currency}{parsed.field_32A.amount:.2f}",
312
+ ]
313
+
314
+ if parsed.field_52A:
315
+ lines.append(f":52A:{parsed.field_52A}")
316
+
317
+ lines.append(f":50K:/{parsed.field_50K}")
318
+
319
+ if parsed.field_56A:
320
+ lines.append(f":56A:{parsed.field_56A}")
321
+
322
+ if parsed.field_57A:
323
+ lines.append(f":57A:{parsed.field_57A}")
324
+
325
+ lines.append(f":59:/{parsed.field_59}")
326
+
327
+ if parsed.field_70:
328
+ lines.append(f":70:{parsed.field_70}")
329
+
330
+ lines.append(f":71A:{parsed.field_71A}")
331
+
332
+ if parsed.field_72:
333
+ lines.append(f":72:{parsed.field_72}")
334
+
335
+ return "\n".join(lines)
336
+
examples/swift_models.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Modèles Pydantic pour messages SWIFT.
3
+
4
+ Ces modèles peuvent être utilisés avec output_type pour valider
5
+ automatiquement la structure des messages SWIFT générés.
6
+ """
7
+
8
+ from pydantic import BaseModel, Field, field_validator
9
+ from datetime import datetime
10
+
11
+
12
+ class SWIFTFielBase(BaseModel):
13
+ """Classe de base pour les champs SWIFT."""
14
+ pass
15
+
16
+
17
+ class MT103Field32A(BaseModel):
18
+ """Champ :32A: Date de valeur, devise, montant."""
19
+ value_date: str = Field(description="Date de valeur YYYYMMDD")
20
+ currency: str = Field(description="Code devise ISO 3 lettres")
21
+ amount: float = Field(description="Montant", gt=0)
22
+
23
+ @field_validator("value_date")
24
+ def validate_date(cls, v):
25
+ if len(v) != 8 or not v.isdigit():
26
+ raise ValueError("Date must be YYYYMMDD format")
27
+ try:
28
+ datetime.strptime(v, "%Y%m%d")
29
+ except ValueError:
30
+ raise ValueError("Invalid date")
31
+ return v
32
+
33
+ @field_validator("currency")
34
+ def validate_currency(cls, v):
35
+ if len(v) != 3 or not v.isalpha():
36
+ raise ValueError("Currency must be 3 letter ISO code")
37
+ return v.upper()
38
+
39
+
40
+ class SWIFTMT103Structured(BaseModel):
41
+ """Message SWIFT MT103 avec validation complète."""
42
+
43
+ field_20: str = Field(description=":20: Référence du transfert")
44
+ field_23B: str = Field(default="CRED", description=":23B: Code instruction")
45
+ field_32A: MT103Field32A = Field(description=":32A: Date, devise, montant")
46
+ field_50K: str = Field(description=":50K: Ordre donneur")
47
+ field_59: str = Field(description=":59: Bénéficiaire")
48
+ field_70: str | None = Field(default=None, description=":70: Information pour bénéficiaire")
49
+ field_71A: str = Field(default="OUR", description=":71A: Frais (OUR/SHA/BEN)")
50
+
51
+ @field_validator("field_71A")
52
+ def validate_charges(cls, v):
53
+ valid = ["OUR", "SHA", "BEN"]
54
+ if v not in valid:
55
+ raise ValueError(f"Charges must be one of {valid}")
56
+ return v
57
+
58
+ def to_swift_format(self) -> str:
59
+ """Convertit en format SWIFT standard."""
60
+ lines = [
61
+ f":20:{self.field_20}",
62
+ f":23B:{self.field_23B}",
63
+ f":32A:{self.field_32A.value_date}{self.field_32A.currency}{self.field_32A.amount:.2f}",
64
+ f":50K:/{self.field_50K}",
65
+ f":59:/{self.field_59}",
66
+ ]
67
+
68
+ if self.field_70:
69
+ lines.append(f":70:{self.field_70}")
70
+
71
+ lines.append(f":71A:{self.field_71A}")
72
+
73
+ return "\n".join(lines)
74
+
75
+
76
+ # Exemple d'utilisation avec validation
77
+ def example_with_validation():
78
+ """Exemple d'utilisation avec validation Pydantic."""
79
+ try:
80
+ mt103 = SWIFTMT103Structured(
81
+ field_20="TXN-2025-001",
82
+ field_32A=MT103Field32A(
83
+ value_date="20250120",
84
+ currency="EUR",
85
+ amount=15000.00
86
+ ),
87
+ field_50K="FR76300040000100000000000123\nORDRE DUPONT",
88
+ field_59="FR1420041010050500013M02606\nBENEFICIAIRE MARTIN",
89
+ field_70="Paiement facture",
90
+ field_71A="OUR"
91
+ )
92
+
93
+ print("✅ Message SWIFT validé:")
94
+ print(mt103.to_swift_format())
95
+
96
+ except Exception as e:
97
+ print(f"❌ Erreur de validation: {e}")
98
+
99
+
100
+
101
+
102
+
103
+
104
+
105
+
106
+
examples/test_swift_parsing.py ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Jeu de tests pour vérifier le parsing de messages SWIFT.
3
+
4
+ Teste différents formats et cas limites pour s'assurer que l'extraction
5
+ fonctionne correctement avec validation Pydantic.
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ # Ajouter le répertoire au path pour les imports
12
+ sys.path.insert(0, str(Path(__file__).parent))
13
+
14
+ from swift_extractor import (
15
+ parse_swift_mt103_advanced,
16
+ SwiftMT103Parsed,
17
+ extract_iban_from_text,
18
+ extract_bic_from_text,
19
+ parse_swift_field_32a,
20
+ )
21
+
22
+
23
+ # ============================================================================
24
+ # MESSAGES SWIFT DE TEST
25
+ # ============================================================================
26
+
27
+ TEST_MESSAGE_1_SIMPLE = """
28
+ :20:NONREF
29
+ :23B:CRED
30
+ :32A:241215EUR15000.00
31
+ :50K:/FR76300040000100000000000123
32
+ ORDRE DUPONT JEAN
33
+ :59:/FR1420041010050500013M02606
34
+ BENEFICIAIRE MARTIN PIERRE
35
+ :70:Paiement facture décembre 2024
36
+ :71A:OUR
37
+ """
38
+
39
+ TEST_MESSAGE_2_FULL_DATE = """
40
+ :20:INVOICE-2024-001
41
+ :23B:CRED
42
+ :32A:20241215EUR25000.50
43
+ :50K:/FR76300040000100000000000123
44
+ ORDRE DUPONT JEAN
45
+ RUE DE LA REPUBLIQUE 123
46
+ 75001 PARIS FRANCE
47
+ :52A:BNPAFRPPXXX
48
+ :56A:SOGEFRPPXXX
49
+ :57A:CRLYFRPPXXX
50
+ :59:/FR1420041010050500013M02606
51
+ BENEFICIAIRE MARTIN PIERRE
52
+ AVENUE DES CHAMPS ELYSEES 456
53
+ 75008 PARIS FRANCE
54
+ :70:Paiement facture décembre 2024
55
+ Référence: INV-001
56
+ :71A:SHA
57
+ :72:/INS/BANQUE INTERMEDIAIRE
58
+ """
59
+
60
+ TEST_MESSAGE_3_MULTILINE = """
61
+ :20:TXN-2025-001
62
+ :23B:CRED
63
+ :32A:250120USD50000.00
64
+ :50K:/US64SVBKUS6SXXX123456789
65
+ COMPANY ABC INC
66
+ 123 MAIN STREET
67
+ NEW YORK NY 10001
68
+ UNITED STATES
69
+ :52A:ABCDUS33XXX
70
+ :59:/GB82WEST12345698765432
71
+ BENEFICIARY XYZ LTD
72
+ 456 HIGH STREET
73
+ LONDON EC1A 1BB
74
+ UNITED KINGDOM
75
+ :70:Payment for services Q1 2025
76
+ Contract reference: CONTRACT-2025-001
77
+ Invoice: INV-2025-042
78
+ :71A:BEN
79
+ :72:/INS/Urgent payment requested
80
+ """
81
+
82
+ TEST_MESSAGE_4_EUROPEAN = """
83
+ :20:PAY-2024-042
84
+ :23B:CRED
85
+ :32A:241231CHF125000.00
86
+ :50K:/CH9300762011623852957
87
+ SWISS COMPANY AG
88
+ BAHNHOFSTRASSE 1
89
+ 8001 ZURICH
90
+ SWITZERLAND
91
+ :52A:UBSWCHZH80A
92
+ :57A:DEUTDEFFXXX
93
+ :59:/DE89370400440532013000
94
+ GERMAN BENEFICIARY GMBH
95
+ FRIEDRICHSTRASSE 100
96
+ 10117 BERLIN
97
+ GERMANY
98
+ :70:Year-end payment 2024
99
+ :71A:OUR
100
+ :72:/INS/Final payment of the year
101
+ """
102
+
103
+ TEST_MESSAGE_5_MINIMAL = """
104
+ :20:MIN-REF-001
105
+ :23B:CRED
106
+ :32A:250101EUR100.00
107
+ :50K:/FR76300040000100000000000123
108
+ CUSTOMER NAME
109
+ :59:/FR1420041010050500013M02606
110
+ BENEFICIARY NAME
111
+ :71A:OUR
112
+ """
113
+
114
+ TEST_MESSAGE_6_WITH_COMMA_ENGLISH = """
115
+ :20:REF-COMMA-ENG
116
+ :23B:CRED
117
+ :32A:250101EUR1,234.56
118
+ :50K:/FR76300040000100000000000123
119
+ ORDERING CUSTOMER
120
+ :59:/FR1420041010050500013M02606
121
+ BENEFICIARY CUSTOMER
122
+ :70:Test with comma as thousands separator (English format)
123
+ :71A:OUR
124
+ """
125
+
126
+ TEST_MESSAGE_6_WITH_COMMA_EUROPEAN = """
127
+ :20:REF-COMMA-EUR
128
+ :23B:CRED
129
+ :32A:250101EUR1.234,56
130
+ :50K:/FR76300040000100000000000123
131
+ ORDERING CUSTOMER
132
+ :59:/FR1420041010050500013M02606
133
+ BENEFICIARY CUSTOMER
134
+ :70:Test with dot for thousands and comma for decimals (European format)
135
+ :71A:OUR
136
+ """
137
+
138
+ TEST_MESSAGE_7_INTERNATIONAL = """
139
+ :20:INTL-TXN-001
140
+ :23B:CRED
141
+ :32A:250215JPY1000000.00
142
+ :50K:/JP9123456789012345678901
143
+ JAPANESE COMPANY CO LTD
144
+ TOKYO 100-0001
145
+ JAPAN
146
+ :52A:MHCBJPJTXXX
147
+ :56A:CHASUS33XXX
148
+ :57A:HSBCGB2LXXX
149
+ :59:/GB29NWBK60161331926819
150
+ UK BENEFICIARY LTD
151
+ LONDON
152
+ :70:International transfer
153
+ :71A:SHA
154
+ :72:/INS/Correspondent bank details
155
+ """
156
+
157
+
158
+ # ============================================================================
159
+ # TESTS
160
+ # ============================================================================
161
+
162
+ def test_field_32a_parsing():
163
+ """Test le parsing du champ :32A: avec différents formats."""
164
+ print("\n" + "=" * 60)
165
+ print("TEST: Parsing champ :32A:")
166
+ print("=" * 60)
167
+
168
+ test_cases = [
169
+ ("241215EUR15000.00", "2024-12-15", "EUR", 15000.0), # YYMMDD
170
+ ("20241215EUR15000.00", "2024-12-15", "EUR", 15000.0), # YYYYMMDD
171
+ ("250101USD100.50", "2025-01-01", "USD", 100.5), # Format court
172
+ ("991231GBP5000.00", "1999-12-31", "GBP", 5000.0), # Année 99 → 1999
173
+ ]
174
+
175
+ for value, expected_date, expected_currency, expected_amount in test_cases:
176
+ try:
177
+ parsed = parse_swift_field_32a(value)
178
+ assert parsed.value_date == expected_date.replace("-", ""), \
179
+ f"Date mismatch: {parsed.value_date} != {expected_date}"
180
+ assert parsed.currency == expected_currency, \
181
+ f"Currency mismatch: {parsed.currency} != {expected_currency}"
182
+ assert parsed.amount == expected_amount, \
183
+ f"Amount mismatch: {parsed.amount} != {expected_amount}"
184
+ print(f"✅ {value} → {parsed.value_date} {parsed.currency} {parsed.amount}")
185
+ except Exception as e:
186
+ print(f"❌ {value} → ERREUR: {e}")
187
+
188
+
189
+ def test_iban_extraction():
190
+ """Test l'extraction d'IBAN depuis du texte."""
191
+ print("\n" + "=" * 60)
192
+ print("TEST: Extraction IBAN")
193
+ print("=" * 60)
194
+
195
+ test_cases = [
196
+ ("/FR76 3000 4000 0100 0000 0000 123", "FR76300040000100000000000123"),
197
+ ("FR1420041010050500013M02606", "FR1420041010050500013M02606"),
198
+ ("Compte: GB82WEST12345698765432", "GB82WEST12345698765432"),
199
+ ("IBAN: CH9300762011623852957 dans le texte", "CH9300762011623852957"),
200
+ ]
201
+
202
+ for text, expected in test_cases:
203
+ iban = extract_iban_from_text(text)
204
+ if iban == expected:
205
+ print(f"✅ '{text[:40]}...' → {iban}")
206
+ else:
207
+ print(f"❌ '{text[:40]}...' → {iban} (attendu: {expected})")
208
+
209
+
210
+ def test_bic_extraction():
211
+ """Test l'extraction de BIC depuis du texte."""
212
+ print("\n" + "=" * 60)
213
+ print("TEST: Extraction BIC")
214
+ print("=" * 60)
215
+
216
+ test_cases = [
217
+ ("BNPAFRPPXXX", "BNPAFRPPXXX"),
218
+ ("BIC: SOGEFRPPXXX", "SOGEFRPPXXX"),
219
+ ("Bank: ABCDUS33", "ABCDUS33"),
220
+ ("BIC ABCDUS33XXX in text", "ABCDUS33XXX"),
221
+ ]
222
+
223
+ for text, expected in test_cases:
224
+ bic = extract_bic_from_text(text)
225
+ if bic == expected:
226
+ print(f"✅ '{text}' → {bic}")
227
+ else:
228
+ print(f"❌ '{text}' → {bic} (attendu: {expected})")
229
+
230
+
231
+ def test_swift_parsing(message_name: str, message: str, description: str = ""):
232
+ """Test le parsing d'un message SWIFT complet."""
233
+ print(f"\n{'=' * 60}")
234
+ print(f"TEST: {message_name}")
235
+ if description:
236
+ print(f"Description: {description}")
237
+ print("=" * 60)
238
+
239
+ try:
240
+ parsed = parse_swift_mt103_advanced(message)
241
+
242
+ print(f"✅ Parsing réussi!")
243
+ print(f" Référence: {parsed.field_20}")
244
+ print(f" Date: {parsed.field_32A.value_date}")
245
+ print(f" Devise: {parsed.field_32A.currency}")
246
+ print(f" Montant: {parsed.field_32A.amount:,.2f} {parsed.field_32A.currency}")
247
+
248
+ if parsed.ordering_customer_account:
249
+ print(f" IBAN ordonnateur: {parsed.ordering_customer_account}")
250
+ if parsed.beneficiary_account:
251
+ print(f" IBAN bénéficiaire: {parsed.beneficiary_account}")
252
+ if parsed.field_52A:
253
+ print(f" BIC banque ordonnateur: {parsed.field_52A}")
254
+ if parsed.field_56A:
255
+ print(f" BIC banque intermédiaire: {parsed.field_56A}")
256
+ if parsed.field_57A:
257
+ print(f" BIC banque bénéficiaire: {parsed.field_57A}")
258
+ if parsed.field_70:
259
+ print(f" Motif: {parsed.field_70[:50]}...")
260
+ print(f" Frais: {parsed.field_71A}")
261
+
262
+ return True
263
+
264
+ except Exception as e:
265
+ print(f"❌ ERREUR: {e}")
266
+ import traceback
267
+ traceback.print_exc()
268
+ return False
269
+
270
+
271
+ def run_all_tests():
272
+ """Exécute tous les tests."""
273
+ print("\n" + "=" * 60)
274
+ print("SUITE DE TESTS - PARSING SWIFT")
275
+ print("=" * 60)
276
+
277
+ results = []
278
+
279
+ # Tests unitaires
280
+ test_field_32a_parsing()
281
+ test_iban_extraction()
282
+ test_bic_extraction()
283
+
284
+ # Tests de parsing complets
285
+ results.append(("Message simple", test_swift_parsing(
286
+ "Message simple (YYMMDD)",
287
+ TEST_MESSAGE_1_SIMPLE,
288
+ "Format basique avec date YYMMDD"
289
+ )))
290
+
291
+ results.append(("Message complet", test_swift_parsing(
292
+ "Message complet (YYYYMMDD)",
293
+ TEST_MESSAGE_2_FULL_DATE,
294
+ "Tous les champs avec banques intermédiaires"
295
+ )))
296
+
297
+ results.append(("Multi-lignes", test_swift_parsing(
298
+ "Message multi-lignes",
299
+ TEST_MESSAGE_3_MULTILINE,
300
+ "Adresses complètes sur plusieurs lignes"
301
+ )))
302
+
303
+ results.append(("Européen", test_swift_parsing(
304
+ "Message européen",
305
+ TEST_MESSAGE_4_EUROPEAN,
306
+ "IBAN suisse et allemand"
307
+ )))
308
+
309
+ results.append(("Minimal", test_swift_parsing(
310
+ "Message minimal",
311
+ TEST_MESSAGE_5_MINIMAL,
312
+ "Uniquement les champs obligatoires"
313
+ )))
314
+
315
+ results.append(("Format anglais", test_swift_parsing(
316
+ "Message avec virgule (format anglais)",
317
+ TEST_MESSAGE_6_WITH_COMMA_ENGLISH,
318
+ "Montant 1,234.56 (virgule = milliers, point = décimales)"
319
+ )))
320
+
321
+ results.append(("Format européen", test_swift_parsing(
322
+ "Message avec virgule (format européen)",
323
+ TEST_MESSAGE_6_WITH_COMMA_EUROPEAN,
324
+ "Montant 1.234,56 (point = milliers, virgule = décimales)"
325
+ )))
326
+
327
+ results.append(("International", test_swift_parsing(
328
+ "Message international",
329
+ TEST_MESSAGE_7_INTERNATIONAL,
330
+ "Transfert intercontinental avec JPY"
331
+ )))
332
+
333
+ # Résumé
334
+ print("\n" + "=" * 60)
335
+ print("RÉSUMÉ DES TESTS")
336
+ print("=" * 60)
337
+
338
+ passed = sum(1 for _, result in results if result)
339
+ total = len(results)
340
+
341
+ for name, result in results:
342
+ status = "✅ PASSÉ" if result else "❌ ÉCHOUÉ"
343
+ print(f"{status}: {name}")
344
+
345
+ print(f"\nTotal: {passed}/{total} tests réussis")
346
+
347
+ if passed == total:
348
+ print("\n🎉 Tous les tests sont passés!")
349
+ else:
350
+ print(f"\n��️ {total - passed} test(s) ont échoué")
351
+
352
+
353
+ if __name__ == "__main__":
354
+ run_all_tests()
355
+
pydanticai_app/__init__.py ADDED
File without changes
pydanticai_app/agents.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PydanticAI agents for finance questions."""
2
+
3
+ from pydantic import BaseModel, Field
4
+ from pydantic_ai import Agent, ModelSettings
5
+
6
+ from pydanticai_app.models import finance_model
7
+ from pydanticai_app.config import settings
8
+
9
+
10
+ class FinanceAnswer(BaseModel):
11
+ """Response model for finance questions."""
12
+ answer: str = Field(description="The answer to the finance question in French")
13
+ confidence: float = Field(description="Confidence level between 0 and 1", ge=0.0, le=1.0)
14
+ key_terms: list[str] = Field(description="List of key financial terms mentioned in the answer")
15
+
16
+
17
+ # Model settings for reasoning models
18
+ # Qwen3 uses <think> tags which consume 40-60% of tokens
19
+ # Increase max_tokens to allow complete responses
20
+ agent_model_settings = ModelSettings(
21
+ max_output_tokens=settings.max_tokens,
22
+ )
23
+
24
+ # Create agent for French finance questions
25
+ # Note: output_type will be specified at runtime in the endpoint
26
+ # Note: max_tokens is set via model_settings for reasoning models (<think> tags)
27
+ finance_agent = Agent(
28
+ finance_model,
29
+ model_settings=agent_model_settings,
30
+ system_prompt=(
31
+ "Vous êtes un assistant financier expert spécialisé dans la terminologie "
32
+ "financière française. Répondez TOUJOURS en français, de manière claire, "
33
+ "précise et concise. Fournissez des explications complètes mais sans "
34
+ "développements excessifs.\n\n"
35
+ "Pour chaque réponse, identifiez les termes clés financiers mentionnés "
36
+ "et estimez votre niveau de confiance dans la réponse (entre 0 et 1).\n\n"
37
+ "Note: Vous avez suffisamment de tokens (max_tokens={}) pour fournir des réponses complètes "
38
+ "incluant votre raisonnement.".format(settings.max_tokens)
39
+ ),
40
+ )
41
+
pydanticai_app/config.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Application configuration."""
2
+
3
+ from pydantic_settings import BaseSettings, SettingsConfigDict
4
+
5
+
6
+ class Settings(BaseSettings):
7
+ """Application settings."""
8
+
9
+ # Hugging Face Space OpenAI API endpoint
10
+ hf_space_url: str = "https://jeanbaptdzd-open-finance-llm-8b.hf.space"
11
+
12
+ # OpenAI-compatible API settings
13
+ api_key: str = "not-needed" # No authentication required
14
+ model_name: str = "DragonLLM/qwen3-8b-fin-v1.0"
15
+
16
+ # API configuration
17
+ timeout: float = 120.0
18
+ max_retries: int = 3
19
+
20
+ # Generation settings for reasoning models
21
+ # Qwen3 uses <think> tags which consume 40-60% of tokens
22
+ # Increase max_tokens to allow complete responses
23
+ max_tokens: int = 1500 # Increased for reasoning models (was default ~800-1000)
24
+
25
+ # Context window limits for Qwen-3 8B
26
+ # Base context window: 32,768 tokens (32K)
27
+ # Extended with YaRN: up to 128,000 tokens (128K)
28
+ # Current max_tokens is for generation, context input can use up to ~30K tokens
29
+
30
+ # Generation limits
31
+ # Maximum theoretical generation: 20,000 tokens
32
+ # Practical limit depends on: context_window - input_tokens - safety_margin
33
+ # With typical input (~500 tokens), can generate up to ~30K tokens
34
+ max_generation_limit: int = 20000 # Theoretical maximum (rarely needed)
35
+
36
+ model_config = SettingsConfigDict(
37
+ env_file=".env",
38
+ env_file_encoding="utf-8",
39
+ extra="ignore",
40
+ )
41
+
42
+
43
+ settings = Settings()
44
+
pydanticai_app/main.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Main FastAPI application entry point."""
2
+
3
+ from fastapi import FastAPI, HTTPException
4
+ from pydantic import BaseModel
5
+
6
+ from pydanticai_app.agents import FinanceAnswer, finance_agent
7
+ from pydanticai_app.config import settings
8
+ from pydanticai_app.utils import extract_answer_from_reasoning, extract_key_terms
9
+
10
+ app = FastAPI(
11
+ title="Open Finance PydanticAI API",
12
+ description="Open Finance API using PydanticAI for LLM inference",
13
+ version="0.1.0"
14
+ )
15
+
16
+
17
+ class QuestionRequest(BaseModel):
18
+ """Request model for finance questions."""
19
+ question: str
20
+
21
+
22
+ class QuestionResponse(BaseModel):
23
+ """Response model for finance questions."""
24
+ answer: str
25
+ confidence: float
26
+ key_terms: list[str]
27
+
28
+
29
+ @app.get("/")
30
+ async def root():
31
+ """Root endpoint."""
32
+ return {
33
+ "status": "ok",
34
+ "service": "Open Finance PydanticAI API",
35
+ "version": "0.1.0",
36
+ "model_source": settings.hf_space_url,
37
+ "model": settings.model_name,
38
+ }
39
+
40
+
41
+ @app.get("/health")
42
+ async def health():
43
+ """Health check endpoint."""
44
+ return {"status": "healthy"}
45
+
46
+
47
+ @app.post("/ask", response_model=QuestionResponse)
48
+ async def ask_question(request: QuestionRequest):
49
+ """Ask a finance question to the AI agent.
50
+
51
+ Handles reasoning model responses by extracting the final answer
52
+ from <think> tags.
53
+ """
54
+ try:
55
+ # Run agent with simple text output (reasoning models return text with tags)
56
+ result = await finance_agent.run(request.question)
57
+
58
+ # Get the raw response text from AgentRunResult
59
+ raw_response = result.output if hasattr(result, 'output') else str(result)
60
+
61
+ # Extract answer from reasoning tags (<think> tags)
62
+ clean_answer = extract_answer_from_reasoning(str(raw_response))
63
+
64
+ # Extract key terms from the cleaned answer
65
+ key_terms = extract_key_terms(clean_answer)
66
+
67
+ # Estimate confidence based on answer quality
68
+ confidence = 0.9 if clean_answer and len(clean_answer) > 50 else 0.7
69
+
70
+ return QuestionResponse(
71
+ answer=clean_answer,
72
+ confidence=confidence,
73
+ key_terms=key_terms,
74
+ )
75
+ except Exception as e:
76
+ raise HTTPException(status_code=500, detail=f"Error processing question: {str(e)}")
77
+
pydanticai_app/models.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PydanticAI model configuration."""
2
+
3
+ from pydantic_ai.models.openai import OpenAIModel
4
+ from pydantic_ai.providers.openai import OpenAIProvider
5
+
6
+ from pydanticai_app.config import settings
7
+
8
+ # Create PydanticAI model using OpenAI-compatible endpoint from Hugging Face Space
9
+ # The model name will be sent in the request, but the actual model is determined by the HF Space
10
+ # Note: max_tokens will be set at the Agent level, not here
11
+ finance_model = OpenAIModel(
12
+ model_name="gpt-3.5-turbo", # Model name for API compatibility (HF Space will use its own model)
13
+ provider=OpenAIProvider(
14
+ base_url=f"{settings.hf_space_url}/v1",
15
+ api_key=settings.api_key,
16
+ ),
17
+ )
18
+
pydanticai_app/utils.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Utility functions for handling reasoning model responses."""
2
+
3
+ import re
4
+
5
+
6
+ def extract_answer_from_reasoning(response: str) -> str:
7
+ """Extract the final answer from a response containing reasoning tags.
8
+
9
+ The Qwen3 model returns responses in the format:
10
+ <think>...reasoning...</think>
11
+ Final answer here...
12
+
13
+ Or sometimes just the reasoning tags without closing tag.
14
+ This function extracts only the final answer part.
15
+ """
16
+ if not response:
17
+ return ""
18
+
19
+ # Method 1: Split on </think> tag (most common format)
20
+ if "</think>" in response:
21
+ parts = response.split("</think>", 1)
22
+ if len(parts) > 1:
23
+ return parts[1].strip()
24
+
25
+ # Method 2: Remove reasoning tags and their content
26
+ # Match <think>...</think> (case insensitive, multi-line)
27
+ cleaned = re.sub(
28
+ r'<think>.*?</think>',
29
+ '',
30
+ response,
31
+ flags=re.DOTALL | re.IGNORECASE
32
+ )
33
+
34
+ # Clean up any remaining whitespace
35
+ cleaned = cleaned.strip()
36
+
37
+ # If we removed everything, return original (fallback)
38
+ if not cleaned:
39
+ return response.strip()
40
+
41
+ return cleaned
42
+
43
+
44
+ def extract_key_terms(text: str) -> list[str]:
45
+ """Extract key financial terms from text.
46
+
47
+ This is a simple heuristic - could be improved with NLP.
48
+ """
49
+ # Common French financial terms patterns
50
+ financial_patterns = [
51
+ r'\bcrédit\b', r'\bprêt\b', r'\bdette\b', r'\bintérêt\b',
52
+ r'\btaux\b', r'\bcapital\b', r'\bdividende\b', r'\baction\b',
53
+ r'\bobligation\b', r'\bfonds\b', r'\bépargne\b', r'\binvestissement\b',
54
+ r'\bhypothèque\b', r'\bamortissement\b', r'\bvalorisation\b',
55
+ r'\bdate de valeur\b', r'\bescompte\b', r'\bconsignation\b',
56
+ r'\bmain levée\b', r'\bséquestre\b', r'\bnantissement\b',
57
+ ]
58
+
59
+ found_terms = []
60
+ text_lower = text.lower()
61
+
62
+ for pattern in financial_patterns:
63
+ if re.search(pattern, text_lower):
64
+ # Extract the matched term
65
+ match = re.search(pattern, text, re.IGNORECASE)
66
+ if match:
67
+ term = match.group(0).strip()
68
+ if term not in found_terms:
69
+ found_terms.append(term)
70
+
71
+ return found_terms[:10] # Limit to 10 terms
72
+
quick_test.py DELETED
@@ -1,54 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Quick test of Space API"""
3
- import httpx
4
- import sys
5
-
6
- SPACE_URL = "https://jeanbaptdzd-open-finance-llm-8b.hf.space"
7
-
8
- try:
9
- # Test root endpoint
10
- r = httpx.get(f"{SPACE_URL}/", timeout=10)
11
- if r.status_code == 200:
12
- data = r.json()
13
- print(f"✓ Root endpoint: {data.get('backend', 'unknown')}")
14
- print(f" Model: {data.get('model', 'unknown')}")
15
- else:
16
- print(f"✗ Root endpoint failed: {r.status_code}")
17
- sys.exit(1)
18
-
19
- # Test models endpoint
20
- r = httpx.get(f"{SPACE_URL}/v1/models", timeout=10)
21
- if r.status_code == 200:
22
- data = r.json()
23
- models = data.get('data', [])
24
- print(f"✓ Models endpoint: {len(models)} model(s)")
25
- else:
26
- print(f"✗ Models endpoint failed: {r.status_code}")
27
- sys.exit(1)
28
-
29
- # Test chat completion (short)
30
- r = httpx.post(
31
- f"{SPACE_URL}/v1/chat/completions",
32
- json={
33
- "model": "DragonLLM/qwen3-8b-fin-v1.0",
34
- "messages": [{"role": "user", "content": "Say hello"}],
35
- "max_tokens": 50
36
- },
37
- timeout=60
38
- )
39
- if r.status_code == 200:
40
- data = r.json()
41
- content = data['choices'][0]['message']['content']
42
- print(f"✓ Chat completion: {len(content)} chars")
43
- print(f" Preview: {content[:50]}...")
44
- else:
45
- print(f"✗ Chat completion failed: {r.status_code}")
46
- print(f" Response: {r.text[:200]}")
47
- sys.exit(1)
48
-
49
- print("\n✓ All tests passed! Space is working.")
50
-
51
- except Exception as e:
52
- print(f"✗ Error: {e}")
53
- sys.exit(1)
54
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test_eos_fix.py DELETED
@@ -1,148 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Test that the EOS token fix is working properly
4
- Verify: no regressions, better completion, proper finish_reason
5
- """
6
- import httpx
7
- import json
8
- import time
9
-
10
- BASE_URL = "https://jeanbaptdzd-open-finance-llm-8b.hf.space"
11
-
12
- def check_space_status():
13
- """Check if Space is running"""
14
- try:
15
- response = httpx.get(f"{BASE_URL}/", timeout=10.0)
16
- data = response.json()
17
- return data.get("status") == "ok" and data.get("backend") == "Transformers"
18
- except:
19
- return False
20
-
21
- print("="*80)
22
- print("TESTING EOS TOKEN FIX")
23
- print("="*80)
24
-
25
- if not check_space_status():
26
- print("❌ Space not ready. Please wait for rebuild.")
27
- exit(1)
28
-
29
- print("✅ Space is ready\n")
30
-
31
- # Test 1: Check finish_reason is accurate
32
- print("[TEST 1] Verify finish_reason accuracy")
33
- print("-" * 80)
34
-
35
- response = httpx.post(
36
- f"{BASE_URL}/v1/chat/completions",
37
- json={
38
- "model": "DragonLLM/qwen3-8b-fin-v1.0",
39
- "messages": [{"role": "user", "content": "What is 2+2? Answer in 5 words."}],
40
- "max_tokens": 50,
41
- "temperature": 0.3
42
- },
43
- timeout=60.0
44
- )
45
-
46
- data = response.json()
47
- finish = data["choices"][0]["finish_reason"]
48
- content = data["choices"][0]["message"]["content"]
49
- tokens = data.get("usage", {}).get("completion_tokens", 0)
50
-
51
- print(f"Max tokens: 50")
52
- print(f"Generated: {tokens} tokens")
53
- print(f"Finish reason: {finish}")
54
- print(f"Response: {content[:150]}...")
55
-
56
- if finish == "stop" and tokens < 50:
57
- print("✅ PASS: Stopped naturally with EOS token (not length limit)")
58
- elif finish == "length" and tokens >= 50:
59
- print("✅ PASS: Correctly detected length limit")
60
- else:
61
- print(f"⚠️ Unexpected: finish={finish}, tokens={tokens}")
62
-
63
- # Test 2: Check complete French answer
64
- print("\n[TEST 2] Complete French answer")
65
- print("-" * 80)
66
-
67
- response = httpx.post(
68
- f"{BASE_URL}/v1/chat/completions",
69
- json={
70
- "model": "DragonLLM/qwen3-8b-fin-v1.0",
71
- "messages": [{"role": "user", "content": "Qu'est-ce qu'une obligation? Soyez concis."}],
72
- "max_tokens": 300,
73
- "temperature": 0.3
74
- },
75
- timeout=60.0
76
- )
77
-
78
- data = response.json()
79
- content = data["choices"][0]["message"]["content"]
80
- finish = data["choices"][0]["finish_reason"]
81
- tokens = data.get("usage", {}).get("completion_tokens", 0)
82
-
83
- # Extract answer
84
- if "</think>" in content:
85
- answer = content.split("</think>")[1].strip()
86
- else:
87
- answer = content
88
-
89
- print(f"Generated: {tokens} tokens")
90
- print(f"Finish reason: {finish}")
91
- print(f"\nFull answer:\n{answer}\n")
92
-
93
- # Check completeness
94
- ends_properly = answer.rstrip().endswith((".", "!", "?", ")", "]"))
95
- has_french = any(c in answer for c in ["é", "è", "à", "ç"])
96
-
97
- print(f"Ends properly: {ends_properly}")
98
- print(f"Is French: {has_french}")
99
- print(f"Finish: {finish}")
100
-
101
- if ends_properly and finish == "stop" and has_french:
102
- print("✅ PASS: Complete French answer with proper EOS")
103
- else:
104
- print(f"⚠️ Check: ends={ends_properly}, finish={finish}, french={has_french}")
105
-
106
- # Test 3: Long answer completeness
107
- print("\n[TEST 3] Long answer completeness")
108
- print("-" * 80)
109
-
110
- response = httpx.post(
111
- f"{BASE_URL}/v1/chat/completions",
112
- json={
113
- "model": "DragonLLM/qwen3-8b-fin-v1.0",
114
- "messages": [{"role": "user", "content": "Expliquez en détail le nantissement de compte-titres."}],
115
- "temperature": 0.3
116
- # Use default max_tokens (1500)
117
- },
118
- timeout=90.0
119
- )
120
-
121
- data = response.json()
122
- content = data["choices"][0]["message"]["content"]
123
- finish = data["choices"][0]["finish_reason"]
124
- tokens = data.get("usage", {}).get("completion_tokens", 0)
125
-
126
- if "</think>" in content:
127
- answer = content.split("</think>")[1].strip()
128
- else:
129
- answer = content
130
-
131
- print(f"Generated: {tokens} tokens (default max: 1500)")
132
- print(f"Finish reason: {finish}")
133
- print(f"Answer length: {len(answer)} chars")
134
- print(f"Last 150 chars: ...{answer[-150:]}")
135
-
136
- if finish == "stop":
137
- print("✅ PASS: Model stopped naturally at EOS (complete answer)")
138
- elif finish == "length":
139
- print(f"⚠️ Hit token limit - may need higher max_tokens for complex questions")
140
- else:
141
- print(f"❌ Unexpected finish_reason: {finish}")
142
-
143
- print("\n" + "="*80)
144
- print("SUMMARY")
145
- print("="*80)
146
- print("If all tests show 'stop' finish_reason and proper sentence endings,")
147
- print("the EOS token fix is working correctly!")
148
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test_french_finance.py DELETED
@@ -1,128 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Test French finance queries against the OpenAI-compatible API.
4
- """
5
-
6
- import os
7
- import sys
8
- import asyncio
9
- import httpx
10
- from typing import Dict, Any
11
-
12
- # Default API URL (can be overridden with API_URL env var)
13
- API_URL = os.getenv("API_URL", "http://localhost:7860/v1")
14
- API_KEY = os.getenv("SERVICE_API_KEY")
15
-
16
- # French finance test questions
17
- FRENCH_QUESTS = [
18
- {
19
- "name": "Obligations",
20
- "question": "Qu'est-ce qu'une obligation?",
21
- "max_tokens": 400,
22
- },
23
- {
24
- "name": "SICAV",
25
- "question": "Qu'est-ce qu'une SICAV?",
26
- "max_tokens": 400,
27
- },
28
- {
29
- "name": "CAC 40",
30
- "question": "Expliquez le CAC 40",
31
- "max_tokens": 500,
32
- },
33
- {
34
- "name": "VaR",
35
- "question": "Qu'est-ce que la Value at Risk (VaR) et comment la calcule-t-on?",
36
- "max_tokens": 600,
37
- },
38
- ]
39
-
40
-
41
- async def test_french_query(client: httpx.AsyncClient, test: Dict[str, Any]) -> Dict[str, Any]:
42
- """Test a single French finance query."""
43
- headers = {"Content-Type": "application/json"}
44
- if API_KEY:
45
- headers["x-api-key"] = API_KEY
46
-
47
- payload = {
48
- "model": "DragonLLM/qwen3-8b-fin-v1.0",
49
- "messages": [{"role": "user", "content": test["question"]}],
50
- "temperature": 0.7,
51
- "max_tokens": test["max_tokens"],
52
- }
53
-
54
- try:
55
- response = await client.post(
56
- f"{API_URL}/chat/completions",
57
- json=payload,
58
- headers=headers,
59
- timeout=120.0,
60
- )
61
- response.raise_for_status()
62
- data = response.json()
63
-
64
- return {
65
- "name": test["name"],
66
- "success": True,
67
- "question": test["question"],
68
- "answer": data["choices"][0]["message"]["content"],
69
- "finish_reason": data["choices"][0]["finish_reason"],
70
- "tokens": data["usage"]["completion_tokens"],
71
- "total_tokens": data["usage"]["total_tokens"],
72
- }
73
- except Exception as e:
74
- return {
75
- "name": test["name"],
76
- "success": False,
77
- "question": test["question"],
78
- "error": str(e),
79
- }
80
-
81
-
82
- async def main():
83
- """Run all French finance tests."""
84
- print("=" * 70)
85
- print("French Finance Test Suite")
86
- print("=" * 70)
87
- print(f"API URL: {API_URL}")
88
- print()
89
-
90
- async with httpx.AsyncClient() as client:
91
- results = []
92
- for i, test in enumerate(FRENCH_QUESTS, 1):
93
- print(f"[{i}/{len(FRENCH_QUESTS)}] Testing: {test['name']}")
94
- print(f" Question: {test['question']}")
95
- result = await test_french_query(client, test)
96
- results.append(result)
97
-
98
- if result["success"]:
99
- answer_preview = result["answer"][:150] + "..." if len(result["answer"]) > 150 else result["answer"]
100
- print(f" ✓ Success")
101
- print(f" Finish reason: {result['finish_reason']}")
102
- print(f" Tokens: {result['tokens']}")
103
- print(f" Answer preview: {answer_preview}")
104
- else:
105
- print(f" ✗ Failed: {result['error']}")
106
- print()
107
-
108
- # Summary
109
- print("=" * 70)
110
- print("Summary")
111
- print("=" * 70)
112
- passed = sum(1 for r in results if r["success"])
113
- print(f"Passed: {passed}/{len(results)}")
114
-
115
- if passed == len(results):
116
- print("✓ All tests passed!")
117
- return 0
118
- else:
119
- print("✗ Some tests failed")
120
- for r in results:
121
- if not r["success"]:
122
- print(f" - {r['name']}: {r['error']}")
123
- return 1
124
-
125
-
126
- if __name__ == "__main__":
127
- sys.exit(asyncio.run(main()))
128
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test_new_features.py DELETED
@@ -1,214 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Test script for new features: health check, stats, rate limiting."""
3
-
4
- import sys
5
- import time
6
- import httpx
7
- from typing import Dict, Any
8
-
9
-
10
- API_URL = "http://localhost:8080"
11
-
12
-
13
- async def test_health_endpoint(client: httpx.AsyncClient) -> Dict[str, Any]:
14
- """Test health endpoint with model readiness check."""
15
- print("Testing /health endpoint...")
16
- try:
17
- response = await client.get(f"{API_URL}/health")
18
- assert response.status_code == 200, f"Expected 200, got {response.status_code}"
19
- data = response.json()
20
-
21
- # Check required fields
22
- assert "status" in data, "Missing 'status' field"
23
- assert "model_ready" in data, "Missing 'model_ready' field"
24
- assert "service" in data, "Missing 'service' field"
25
-
26
- print(f" ✓ Status: {data['status']}")
27
- print(f" ✓ Model ready: {data['model_ready']}")
28
- print(f" ✓ Service: {data['service']}")
29
-
30
- return {"success": True, "data": data}
31
- except Exception as e:
32
- print(f" ✗ Failed: {e}")
33
- return {"success": False, "error": str(e)}
34
-
35
-
36
- async def test_stats_endpoint(client: httpx.AsyncClient) -> Dict[str, Any]:
37
- """Test stats endpoint."""
38
- print("\nTesting /v1/stats endpoint...")
39
- try:
40
- response = await client.get(f"{API_URL}/v1/stats")
41
- assert response.status_code == 200, f"Expected 200, got {response.status_code}"
42
- data = response.json()
43
-
44
- # Check required fields
45
- required_fields = [
46
- "uptime_seconds", "total_requests", "total_tokens",
47
- "average_total_tokens", "requests_per_hour", "tokens_per_hour"
48
- ]
49
- for field in required_fields:
50
- assert field in data, f"Missing '{field}' field"
51
-
52
- print(f" ✓ Uptime: {data['uptime_seconds']}s ({data.get('uptime_hours', 0):.2f}h)")
53
- print(f" ✓ Total requests: {data['total_requests']}")
54
- print(f" ✓ Total tokens: {data['total_tokens']}")
55
- print(f" ✓ Average tokens: {data['average_total_tokens']:.2f}")
56
- print(f" ✓ Requests/hour: {data['requests_per_hour']:.2f}")
57
- print(f" ✓ Tokens/hour: {data['tokens_per_hour']:.2f}")
58
-
59
- if data.get('requests_by_model'):
60
- print(f" ✓ Models used: {list(data['requests_by_model'].keys())}")
61
-
62
- if data.get('finish_reasons'):
63
- print(f" ✓ Finish reasons: {data['finish_reasons']}")
64
-
65
- return {"success": True, "data": data}
66
- except Exception as e:
67
- print(f" ✗ Failed: {e}")
68
- return {"success": False, "error": str(e)}
69
-
70
-
71
- async def test_rate_limiting(client: httpx.AsyncClient) -> Dict[str, Any]:
72
- """Test rate limiting (should allow requests, check headers)."""
73
- print("\nTesting rate limiting...")
74
- try:
75
- # Make a request to check rate limit headers
76
- response = await client.get(f"{API_URL}/v1/models")
77
- assert response.status_code == 200, f"Expected 200, got {response.status_code}"
78
-
79
- # Check for rate limit headers
80
- headers = response.headers
81
- rate_limit_headers = [
82
- "X-RateLimit-Limit-Minute",
83
- "X-RateLimit-Limit-Hour",
84
- "X-RateLimit-Remaining-Minute",
85
- "X-RateLimit-Remaining-Hour"
86
- ]
87
-
88
- found_headers = []
89
- for header in rate_limit_headers:
90
- if header in headers:
91
- found_headers.append(header)
92
- print(f" ✓ {header}: {headers[header]}")
93
-
94
- if len(found_headers) == len(rate_limit_headers):
95
- print(" ✓ All rate limit headers present")
96
- return {"success": True, "headers": {h: headers[h] for h in rate_limit_headers}}
97
- else:
98
- missing = set(rate_limit_headers) - set(found_headers)
99
- print(f" ⚠ Missing headers: {missing}")
100
- return {"success": False, "error": f"Missing headers: {missing}"}
101
-
102
- except Exception as e:
103
- print(f" ✗ Failed: {e}")
104
- return {"success": False, "error": str(e)}
105
-
106
-
107
- async def test_error_sanitization(client: httpx.AsyncClient) -> Dict[str, Any]:
108
- """Test that error messages are sanitized."""
109
- print("\nTesting error sanitization...")
110
- try:
111
- # Make an invalid request
112
- response = await client.post(
113
- f"{API_URL}/v1/chat/completions",
114
- json={
115
- "model": "test",
116
- "messages": [], # Empty messages should fail
117
- }
118
- )
119
-
120
- assert response.status_code == 400, f"Expected 400, got {response.status_code}"
121
- data = response.json()
122
-
123
- # Check error structure
124
- assert "error" in data, "Missing 'error' field"
125
- assert "message" in data["error"], "Missing 'message' in error"
126
- assert "type" in data["error"], "Missing 'type' in error"
127
-
128
- error_msg = data["error"]["message"]
129
- # Should not contain internal details like file paths, stack traces, etc.
130
- internal_indicators = ["Traceback", "File", "line", ".py", "Exception:"]
131
- for indicator in internal_indicators:
132
- assert indicator.lower() not in error_msg.lower(), f"Error message contains internal details: {indicator}"
133
-
134
- print(f" ✓ Error properly formatted: {error_msg[:100]}")
135
- print(f" ✓ Error type: {data['error']['type']}")
136
-
137
- return {"success": True, "error": data["error"]}
138
- except Exception as e:
139
- print(f" ✗ Failed: {e}")
140
- return {"success": False, "error": str(e)}
141
-
142
-
143
- async def test_root_endpoint(client: httpx.AsyncClient) -> Dict[str, Any]:
144
- """Test root endpoint."""
145
- print("\nTesting / endpoint...")
146
- try:
147
- response = await client.get(f"{API_URL}/")
148
- assert response.status_code == 200, f"Expected 200, got {response.status_code}"
149
- data = response.json()
150
-
151
- assert "status" in data, "Missing 'status' field"
152
- print(f" ✓ Status: {data['status']}")
153
- print(f" ✓ Service: {data.get('service', 'N/A')}")
154
-
155
- return {"success": True, "data": data}
156
- except Exception as e:
157
- print(f" ✗ Failed: {e}")
158
- return {"success": False, "error": str(e)}
159
-
160
-
161
- async def main():
162
- """Run all tests."""
163
- print("=" * 70)
164
- print("Testing New Features")
165
- print("=" * 70)
166
- print(f"API URL: {API_URL}")
167
- print()
168
-
169
- timeout = httpx.Timeout(30.0, connect=10.0)
170
- async with httpx.AsyncClient(timeout=timeout) as client:
171
- results = []
172
-
173
- # Test root endpoint
174
- results.append(await test_root_endpoint(client))
175
-
176
- # Test health endpoint
177
- results.append(await test_health_endpoint(client))
178
-
179
- # Test stats endpoint (before any requests)
180
- results.append(await test_stats_endpoint(client))
181
-
182
- # Test rate limiting
183
- results.append(await test_rate_limiting(client))
184
-
185
- # Test error sanitization
186
- results.append(await test_error_sanitization(client))
187
-
188
- # Test stats endpoint again (after requests)
189
- print("\nTesting /v1/stats endpoint (after requests)...")
190
- results.append(await test_stats_endpoint(client))
191
-
192
- # Summary
193
- print("\n" + "=" * 70)
194
- print("Summary")
195
- print("=" * 70)
196
- passed = sum(1 for r in results if r["success"])
197
- total = len(results)
198
- print(f"Passed: {passed}/{total}")
199
-
200
- if passed == total:
201
- print("✓ All tests passed!")
202
- return 0
203
- else:
204
- print("✗ Some tests failed")
205
- for i, r in enumerate(results, 1):
206
- if not r["success"]:
207
- print(f" Test {i}: {r.get('error', 'Unknown error')}")
208
- return 1
209
-
210
-
211
- if __name__ == "__main__":
212
- import asyncio
213
- sys.exit(asyncio.run(main()))
214
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test_pydanticai.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Test script for PydanticAI integration."""
3
+
4
+ import asyncio
5
+ import sys
6
+ from pydanticai_app.agents import finance_agent
7
+ from pydanticai_app.utils import extract_answer_from_reasoning, extract_key_terms
8
+
9
+
10
+ async def test_finance_agent():
11
+ """Test the finance agent."""
12
+ print("=" * 70)
13
+ print("Testing PydanticAI Finance Agent")
14
+ print("=" * 70)
15
+ print()
16
+
17
+ test_questions = [
18
+ "Qu'est-ce qu'une obligation?",
19
+ "Expliquez le concept de date de valeur.",
20
+ "Qu'est-ce que le CAC 40?",
21
+ ]
22
+
23
+ for i, question in enumerate(test_questions, 1):
24
+ print(f"[{i}/{len(test_questions)}] Question: {question}")
25
+ print("-" * 70)
26
+
27
+ try:
28
+ # Run agent
29
+ result = await finance_agent.run(question)
30
+
31
+ # Get raw response
32
+ raw_response = result.output if hasattr(result, 'output') else str(result)
33
+
34
+ # Extract answer from reasoning tags
35
+ clean_answer = extract_answer_from_reasoning(str(raw_response))
36
+
37
+ # Extract key terms
38
+ key_terms = extract_key_terms(clean_answer)
39
+
40
+ print(f"✅ Response received")
41
+ print(f"Answer length: {len(clean_answer)} chars")
42
+ print(f"Key terms: {key_terms[:5]}")
43
+ print(f"Answer preview: {clean_answer[:200]}...")
44
+ print()
45
+
46
+ except Exception as e:
47
+ print(f"❌ Error: {e}")
48
+ import traceback
49
+ traceback.print_exc()
50
+ print()
51
+ return False
52
+
53
+ print("=" * 70)
54
+ print("✅ All tests passed!")
55
+ print("=" * 70)
56
+ return True
57
+
58
+
59
+ if __name__ == "__main__":
60
+ success = asyncio.run(test_finance_agent())
61
+ sys.exit(0 if success else 1)
62
+
test_regression.py DELETED
@@ -1,118 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Regression test: verify EOS token fix improves completeness without breaking anything
4
- """
5
- import httpx
6
- import json
7
- import time
8
-
9
- BASE_URL = "https://jeanbaptdzd-open-finance-llm-8b.hf.space"
10
-
11
- print("="*80)
12
- print("REGRESSION & IMPROVEMENT TEST")
13
- print("="*80)
14
-
15
- # Test 1: Basic functionality still works
16
- print("\n[1] Basic functionality check")
17
- try:
18
- response = httpx.post(
19
- f"{BASE_URL}/v1/chat/completions",
20
- json={
21
- "model": "DragonLLM/qwen3-8b-fin-v1.0",
22
- "messages": [{"role": "user", "content": "What is 2+2?"}],
23
- "max_tokens": 100,
24
- "temperature": 0.3
25
- },
26
- timeout=30.0
27
- )
28
-
29
- data = response.json()
30
- if "error" not in data:
31
- print(f"✅ Basic request works")
32
- else:
33
- print(f"❌ Error: {data['error']['message']}")
34
- except Exception as e:
35
- print(f"❌ Exception: {e}")
36
-
37
- time.sleep(3)
38
-
39
- # Test 2: French answer with reasonable token limit
40
- print("\n[2] French answer (500 tokens)")
41
- try:
42
- response = httpx.post(
43
- f"{BASE_URL}/v1/chat/completions",
44
- json={
45
- "model": "DragonLLM/qwen3-8b-fin-v1.0",
46
- "messages": [{"role": "user", "content": "Qu'est-ce qu'une obligation? Réponse courte."}],
47
- "max_tokens": 500,
48
- "temperature": 0.3
49
- },
50
- timeout=45.0
51
- )
52
-
53
- data = response.json()
54
- if "error" in data:
55
- print(f"❌ Error: {data['error']['message'][:100]}")
56
- else:
57
- content = data["choices"][0]["message"]["content"]
58
- finish = data["choices"][0]["finish_reason"]
59
- tokens = data.get("usage", {}).get("completion_tokens", 0)
60
-
61
- answer = content.split("</think>")[1].strip() if "</think>" in content else content
62
-
63
- print(f"Tokens: {tokens}/500")
64
- print(f"Finish: {finish}")
65
- print(f"Answer: {answer}")
66
- print(f"Ends properly: {answer.rstrip().endswith(('.', '!', '?'))}")
67
-
68
- if finish == "stop":
69
- print(f"✅ IMPROVEMENT: Stopped naturally at EOS (was hitting length before)")
70
- elif finish == "length":
71
- print(f"⚠️ Still hitting length limit")
72
-
73
- except Exception as e:
74
- print(f"❌ Exception: {e}")
75
-
76
- time.sleep(3)
77
-
78
- # Test 3: Sequential requests (no OOM regression)
79
- print("\n[3] Sequential requests (memory check)")
80
- success = 0
81
- for i in range(1, 4):
82
- try:
83
- response = httpx.post(
84
- f"{BASE_URL}/v1/chat/completions",
85
- json={
86
- "model": "DragonLLM/qwen3-8b-fin-v1.0",
87
- "messages": [{"role": "user", "content": f"Calculate {i}+{i}"}],
88
- "max_tokens": 200,
89
- "temperature": 0.3
90
- },
91
- timeout=30.0
92
- )
93
-
94
- data = response.json()
95
- if "error" not in data:
96
- success += 1
97
- print(f" [{i}] ✅")
98
- else:
99
- if "out of memory" in data["error"]["message"].lower():
100
- print(f" [{i}] ❌ OOM!")
101
- else:
102
- print(f" [{i}] ❌ Error")
103
- time.sleep(2)
104
- except:
105
- print(f" [{i}] ❌ Timeout/Exception")
106
-
107
- if success == 3:
108
- print(f"✅ NO REGRESSION: Memory management still working")
109
- else:
110
- print(f"❌ REGRESSION: Only {success}/3 succeeded")
111
-
112
- print("\n" + "="*80)
113
- print("VERDICT")
114
- print("="*80)
115
- print("If Test 2 shows finish='stop' → EOS fix is working ✅")
116
- print("If Test 2 shows finish='length' → Need more investigation ⚠️")
117
- print("If Test 3 passes → No memory regression ✅")
118
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test_space_api.py DELETED
@@ -1,142 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Test the Hugging Face Space API to verify the refactored code works.
4
- """
5
-
6
- import os
7
- import sys
8
- import asyncio
9
- import httpx
10
- from typing import Dict, Any
11
-
12
- # Space URL - update this if your Space has a different URL
13
- SPACE_URL = os.getenv("SPACE_URL", "https://jeanbaptdzd-open-finance-llm-8b.hf.space/v1")
14
- API_KEY = os.getenv("SERVICE_API_KEY")
15
-
16
-
17
- async def test_endpoint(client: httpx.AsyncClient, name: str, method: str, url: str, **kwargs) -> Dict[str, Any]:
18
- """Test a single API endpoint."""
19
- try:
20
- headers = kwargs.pop("headers", {})
21
- if API_KEY:
22
- headers["x-api-key"] = API_KEY
23
-
24
- if method.upper() == "GET":
25
- response = await client.get(url, headers=headers, timeout=30.0)
26
- elif method.upper() == "POST":
27
- response = await client.post(url, headers=headers, timeout=120.0, **kwargs)
28
- else:
29
- return {"name": name, "success": False, "error": f"Unsupported method: {method}"}
30
-
31
- response.raise_for_status()
32
- return {
33
- "name": name,
34
- "success": True,
35
- "status_code": response.status_code,
36
- "data": response.json() if response.headers.get("content-type", "").startswith("application/json") else response.text[:200],
37
- }
38
- except Exception as e:
39
- return {
40
- "name": name,
41
- "success": False,
42
- "error": str(e),
43
- }
44
-
45
-
46
- async def main():
47
- """Run API tests."""
48
- print("=" * 70)
49
- print("Testing Hugging Face Space API")
50
- print("=" * 70)
51
- print(f"Space URL: {SPACE_URL}")
52
- print()
53
-
54
- async with httpx.AsyncClient() as client:
55
- results = []
56
-
57
- # Test 1: Root endpoint
58
- print("[1/4] Testing root endpoint...")
59
- result = await test_endpoint(client, "Root", "GET", SPACE_URL.replace("/v1", ""))
60
- results.append(result)
61
- if result["success"]:
62
- print(f" ✓ Success: {result.get('data', {}).get('status', 'ok')}")
63
- else:
64
- print(f" ✗ Failed: {result['error']}")
65
- print()
66
-
67
- # Test 2: List models
68
- print("[2/4] Testing /v1/models endpoint...")
69
- result = await test_endpoint(client, "List Models", "GET", f"{SPACE_URL}/models")
70
- results.append(result)
71
- if result["success"]:
72
- models = result.get("data", {}).get("data", [])
73
- print(f" ✓ Success: Found {len(models)} model(s)")
74
- if models:
75
- print(f" Model: {models[0].get('id', 'unknown')}")
76
- else:
77
- print(f" ✗ Failed: {result['error']}")
78
- print()
79
-
80
- # Test 3: Chat completion (simple)
81
- print("[3/4] Testing /v1/chat/completions endpoint...")
82
- result = await test_endpoint(
83
- client,
84
- "Chat Completion",
85
- "POST",
86
- f"{SPACE_URL}/chat/completions",
87
- json={
88
- "model": "DragonLLM/qwen3-8b-fin-v1.0",
89
- "messages": [{"role": "user", "content": "What is compound interest? Answer in one sentence."}],
90
- "temperature": 0.7,
91
- "max_tokens": 100,
92
- }
93
- )
94
- results.append(result)
95
- if result["success"]:
96
- data = result.get("data", {})
97
- content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
98
- tokens = data.get("usage", {}).get("total_tokens", 0)
99
- print(f" ✓ Success: Generated {tokens} tokens")
100
- print(f" Response preview: {content[:100]}...")
101
- else:
102
- print(f" ✗ Failed: {result['error']}")
103
- print()
104
-
105
- # Test 4: Model reload endpoint
106
- print("[4/4] Testing /v1/models/reload endpoint...")
107
- result = await test_endpoint(
108
- client,
109
- "Model Reload",
110
- "POST",
111
- f"{SPACE_URL}/models/reload",
112
- params={"force": False}
113
- )
114
- results.append(result)
115
- if result["success"]:
116
- data = result.get("data", {})
117
- print(f" ✓ Success: {data.get('message', 'OK')}")
118
- else:
119
- print(f" ✗ Failed: {result['error']}")
120
- print()
121
-
122
- # Summary
123
- print("=" * 70)
124
- print("Test Summary")
125
- print("=" * 70)
126
- passed = sum(1 for r in results if r["success"])
127
- print(f"Passed: {passed}/{len(results)}")
128
-
129
- if passed == len(results):
130
- print("✓ All tests passed! The Space is working correctly.")
131
- return 0
132
- else:
133
- print("✗ Some tests failed")
134
- for r in results:
135
- if not r["success"]:
136
- print(f" - {r['name']}: {r['error']}")
137
- return 1
138
-
139
-
140
- if __name__ == "__main__":
141
- sys.exit(asyncio.run(main()))
142
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/performance/__init__.py CHANGED
@@ -6,3 +6,11 @@
6
 
7
 
8
 
 
 
 
 
 
 
 
 
 
6
 
7
 
8
 
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+