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 +0 -1
- CHANGES_SUMMARY.md +0 -312
- CLEANUP_PLAN.md +0 -155
- CLEANUP_SUMMARY.md +0 -190
- CODE_REVIEW_SUMMARY.md +0 -119
- DEPLOYMENT_READY.md +0 -152
- DEPLOYMENT_TEST_GUIDE.md +0 -228
- FINAL_STATUS.md +0 -129
- FINAL_TEST_REPORT.md +0 -261
- README.md +77 -16
- TEST_CODERABBIT.md +0 -40
- docs/generation_limits.md +85 -0
- docs/qwen3_specifications.md +82 -0
- docs/reasoning_models.md +94 -0
- examples/README.md +121 -0
- examples/SWIFT_IMPROVEMENTS.md +157 -0
- examples/agent_1_structured_data.py +78 -0
- examples/agent_2_tools.py +139 -0
- examples/agent_3_multi_step.py +152 -0
- examples/agent_swift.py +540 -0
- examples/agent_with_tools_and_memory.py +368 -0
- examples/memory_strategies.py +365 -0
- examples/swift_extractor.py +336 -0
- examples/swift_models.py +106 -0
- examples/test_swift_parsing.py +355 -0
- pydanticai_app/__init__.py +0 -0
- pydanticai_app/agents.py +41 -0
- pydanticai_app/config.py +44 -0
- pydanticai_app/main.py +77 -0
- pydanticai_app/models.py +18 -0
- pydanticai_app/utils.py +72 -0
- quick_test.py +0 -54
- test_eos_fix.py +0 -148
- test_french_finance.py +0 -128
- test_new_features.py +0 -214
- test_pydanticai.py +62 -0
- test_regression.py +0 -118
- test_space_api.py +0 -142
- tests/performance/__init__.py +8 -0
.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://
|
| 25 |
```
|
| 26 |
|
| 27 |
### Chat Completions
|
| 28 |
```bash
|
| 29 |
-
curl -X POST "https://
|
| 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://
|
| 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
|
| 76 |
|
| 77 |
## Integration
|
| 78 |
|
| 79 |
### PydanticAI
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
```python
|
| 81 |
-
from
|
| 82 |
-
from pydantic_ai.models.openai import OpenAIModel
|
| 83 |
|
| 84 |
-
|
| 85 |
-
"
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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://
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
```
|
| 135 |
|
| 136 |
-
##
|
| 137 |
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|