Commit
·
58ff73c
1
Parent(s):
78ed4ff
Remove PydanticAI integration and examples
Browse files- Remove pydanticai_app/ directory (PydanticAI integration moved to open-finance-pydanticAI repo)
- Remove test_pydanticai.py test script
- Remove examples/ directory with all PydanticAI example scripts
- Update README.md to remove PydanticAI references and focus on OpenAI-compatible API
- Repository now focuses solely on OpenAI-compatible API wrapper
- README.md +2 -21
- examples/README.md +0 -121
- examples/SWIFT_IMPROVEMENTS.md +0 -157
- examples/agent_1_structured_data.py +0 -78
- examples/agent_2_tools.py +0 -139
- examples/agent_3_multi_step.py +0 -152
- examples/agent_swift.py +0 -540
- examples/agent_with_tools_and_memory.py +0 -368
- examples/memory_strategies.py +0 -365
- examples/swift_extractor.py +0 -336
- examples/swift_models.py +0 -106
- examples/test_swift_parsing.py +0 -355
- pydanticai_app/__init__.py +0 -0
- pydanticai_app/agents.py +0 -41
- pydanticai_app/config.py +0 -44
- pydanticai_app/main.py +0 -77
- pydanticai_app/models.py +0 -18
- pydanticai_app/utils.py +0 -72
- test_pydanticai.py +0 -62
- test_space_basic.py +242 -0
- test_space_with_tools.py +240 -0
README.md
CHANGED
|
@@ -25,7 +25,8 @@ This service provides an OpenAI-compatible API for the DragonLLM Qwen3-8B financ
|
|
| 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 |
-
- ✅ **
|
|
|
|
| 29 |
|
| 30 |
## API Endpoints
|
| 31 |
|
|
@@ -96,21 +97,6 @@ Token priority: `HF_TOKEN_LC2` > `HF_TOKEN_LC` > `HF_TOKEN` > `HUGGING_FACE_HUB_
|
|
| 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
|
|
@@ -177,9 +163,6 @@ 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
|
|
@@ -192,8 +175,6 @@ python test_pydanticai.py
|
|
| 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
|
|
|
|
| 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 |
+
- ✅ **Tool Calls Support** - OpenAI-compatible tool/function calling
|
| 29 |
+
- ✅ **Structured Outputs** - JSON format support via response_format
|
| 30 |
|
| 31 |
## API Endpoints
|
| 32 |
|
|
|
|
| 97 |
|
| 98 |
## Integration
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
### OpenAI SDK
|
| 101 |
|
| 102 |
```python
|
|
|
|
| 163 |
|
| 164 |
# Test deployment
|
| 165 |
./test_deployment.sh
|
|
|
|
|
|
|
|
|
|
| 166 |
```
|
| 167 |
|
| 168 |
## Project Structure
|
|
|
|
| 175 |
│ ├── providers/ # Model providers
|
| 176 |
│ ├── middleware/ # Rate limiting, auth
|
| 177 |
│ └── utils/ # Utilities, stats tracking
|
|
|
|
|
|
|
| 178 |
├── docs/ # Documentation
|
| 179 |
├── tests/ # Test suite
|
| 180 |
└── scripts/ # Utility scripts
|
examples/README.md
DELETED
|
@@ -1,121 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,157 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,78 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,139 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,152 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,540 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,368 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,365 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,336 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,106 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,355 +0,0 @@
|
|
| 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
DELETED
|
File without changes
|
pydanticai_app/agents.py
DELETED
|
@@ -1,41 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,44 +0,0 @@
|
|
| 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/Qwen-Open-Finance-R-8B"
|
| 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
DELETED
|
@@ -1,77 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,18 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,72 +0,0 @@
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test_pydanticai.py
DELETED
|
@@ -1,62 +0,0 @@
|
|
| 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_space_basic.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Basic tests for the Hugging Face Space API."""
|
| 3 |
+
|
| 4 |
+
import requests
|
| 5 |
+
import json
|
| 6 |
+
import time
|
| 7 |
+
from typing import Dict, Any
|
| 8 |
+
|
| 9 |
+
SPACE_URL = "https://jeanbaptdzd-open-finance-llm-8b.hf.space"
|
| 10 |
+
API_BASE = f"{SPACE_URL}/v1"
|
| 11 |
+
|
| 12 |
+
def test_health():
|
| 13 |
+
"""Test if the Space is accessible."""
|
| 14 |
+
print("=" * 60)
|
| 15 |
+
print("Test 1: Health Check")
|
| 16 |
+
print("=" * 60)
|
| 17 |
+
try:
|
| 18 |
+
response = requests.get(f"{SPACE_URL}/health", timeout=10)
|
| 19 |
+
print(f"Status Code: {response.status_code}")
|
| 20 |
+
if response.status_code == 200:
|
| 21 |
+
print(f"Response: {response.json()}")
|
| 22 |
+
print("✅ Health check passed")
|
| 23 |
+
return True
|
| 24 |
+
else:
|
| 25 |
+
print(f"❌ Health check failed: {response.status_code}")
|
| 26 |
+
return False
|
| 27 |
+
except Exception as e:
|
| 28 |
+
print(f"❌ Health check error: {e}")
|
| 29 |
+
return False
|
| 30 |
+
|
| 31 |
+
def test_list_models():
|
| 32 |
+
"""Test listing available models."""
|
| 33 |
+
print("\n" + "=" * 60)
|
| 34 |
+
print("Test 2: List Models")
|
| 35 |
+
print("=" * 60)
|
| 36 |
+
try:
|
| 37 |
+
response = requests.get(f"{API_BASE}/models", timeout=10)
|
| 38 |
+
print(f"Status Code: {response.status_code}")
|
| 39 |
+
if response.status_code == 200:
|
| 40 |
+
data = response.json()
|
| 41 |
+
print(f"Response: {json.dumps(data, indent=2)}")
|
| 42 |
+
print("✅ List models passed")
|
| 43 |
+
return True
|
| 44 |
+
else:
|
| 45 |
+
print(f"❌ List models failed: {response.status_code}")
|
| 46 |
+
print(f"Response: {response.text}")
|
| 47 |
+
return False
|
| 48 |
+
except Exception as e:
|
| 49 |
+
print(f"❌ List models error: {e}")
|
| 50 |
+
return False
|
| 51 |
+
|
| 52 |
+
def test_simple_chat():
|
| 53 |
+
"""Test basic chat completion."""
|
| 54 |
+
print("\n" + "=" * 60)
|
| 55 |
+
print("Test 3: Simple Chat Completion")
|
| 56 |
+
print("=" * 60)
|
| 57 |
+
try:
|
| 58 |
+
payload = {
|
| 59 |
+
"model": "dragon-llm-open-finance",
|
| 60 |
+
"messages": [
|
| 61 |
+
{"role": "user", "content": "Bonjour, dis-moi simplement 'test réussi'"}
|
| 62 |
+
],
|
| 63 |
+
"temperature": 0.7,
|
| 64 |
+
"max_tokens": 50
|
| 65 |
+
}
|
| 66 |
+
print(f"Request: {json.dumps(payload, indent=2, ensure_ascii=False)}")
|
| 67 |
+
response = requests.post(
|
| 68 |
+
f"{API_BASE}/chat/completions",
|
| 69 |
+
json=payload,
|
| 70 |
+
timeout=120 # Increased timeout for model loading/generation
|
| 71 |
+
)
|
| 72 |
+
print(f"Status Code: {response.status_code}")
|
| 73 |
+
if response.status_code == 200:
|
| 74 |
+
data = response.json()
|
| 75 |
+
print(f"Response: {json.dumps(data, indent=2, ensure_ascii=False)}")
|
| 76 |
+
if "choices" in data and len(data["choices"]) > 0:
|
| 77 |
+
content = data["choices"][0]["message"].get("content", "")
|
| 78 |
+
print(f"\n✅ Chat completion passed")
|
| 79 |
+
print(f"Generated text: {content[:100]}...")
|
| 80 |
+
return True
|
| 81 |
+
else:
|
| 82 |
+
print("❌ No choices in response")
|
| 83 |
+
return False
|
| 84 |
+
else:
|
| 85 |
+
print(f"❌ Chat completion failed: {response.status_code}")
|
| 86 |
+
print(f"Response: {response.text}")
|
| 87 |
+
return False
|
| 88 |
+
except Exception as e:
|
| 89 |
+
print(f"❌ Chat completion error: {e}")
|
| 90 |
+
import traceback
|
| 91 |
+
traceback.print_exc()
|
| 92 |
+
return False
|
| 93 |
+
|
| 94 |
+
def test_tool_choice_required():
|
| 95 |
+
"""Test tool_choice='required' (our fix)."""
|
| 96 |
+
print("\n" + "=" * 60)
|
| 97 |
+
print("Test 4: tool_choice='required' (Fix Verification)")
|
| 98 |
+
print("=" * 60)
|
| 99 |
+
try:
|
| 100 |
+
payload = {
|
| 101 |
+
"model": "dragon-llm-open-finance",
|
| 102 |
+
"messages": [
|
| 103 |
+
{"role": "user", "content": "Dis bonjour"}
|
| 104 |
+
],
|
| 105 |
+
"tools": [
|
| 106 |
+
{
|
| 107 |
+
"type": "function",
|
| 108 |
+
"function": {
|
| 109 |
+
"name": "say_hello",
|
| 110 |
+
"description": "Say hello",
|
| 111 |
+
"parameters": {
|
| 112 |
+
"type": "object",
|
| 113 |
+
"properties": {
|
| 114 |
+
"name": {
|
| 115 |
+
"type": "string",
|
| 116 |
+
"description": "Name to greet"
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
],
|
| 123 |
+
"tool_choice": "required", # This should not cause 422 error anymore
|
| 124 |
+
"temperature": 0.7,
|
| 125 |
+
"max_tokens": 100
|
| 126 |
+
}
|
| 127 |
+
print(f"Request: tool_choice='required'")
|
| 128 |
+
response = requests.post(
|
| 129 |
+
f"{API_BASE}/chat/completions",
|
| 130 |
+
json=payload,
|
| 131 |
+
timeout=120 # Increased timeout for model loading/generation
|
| 132 |
+
)
|
| 133 |
+
print(f"Status Code: {response.status_code}")
|
| 134 |
+
if response.status_code == 200:
|
| 135 |
+
data = response.json()
|
| 136 |
+
print(f"✅ tool_choice='required' accepted (no 422 error)")
|
| 137 |
+
print(f"Response keys: {list(data.keys())}")
|
| 138 |
+
return True
|
| 139 |
+
elif response.status_code == 422:
|
| 140 |
+
print(f"❌ Still getting 422 error with tool_choice='required'")
|
| 141 |
+
print(f"Response: {response.text}")
|
| 142 |
+
return False
|
| 143 |
+
else:
|
| 144 |
+
print(f"⚠️ Unexpected status code: {response.status_code}")
|
| 145 |
+
print(f"Response: {response.text}")
|
| 146 |
+
return False
|
| 147 |
+
except Exception as e:
|
| 148 |
+
print(f"❌ tool_choice test error: {e}")
|
| 149 |
+
import traceback
|
| 150 |
+
traceback.print_exc()
|
| 151 |
+
return False
|
| 152 |
+
|
| 153 |
+
def test_response_format():
|
| 154 |
+
"""Test response_format for structured outputs (our fix)."""
|
| 155 |
+
print("\n" + "=" * 60)
|
| 156 |
+
print("Test 5: response_format (Fix Verification)")
|
| 157 |
+
print("=" * 60)
|
| 158 |
+
try:
|
| 159 |
+
payload = {
|
| 160 |
+
"model": "dragon-llm-open-finance",
|
| 161 |
+
"messages": [
|
| 162 |
+
{"role": "user", "content": "Donne-moi un nombre aléatoire entre 1 et 10 au format JSON: {\"nombre\": X}"}
|
| 163 |
+
],
|
| 164 |
+
"response_format": {"type": "json_object"},
|
| 165 |
+
"temperature": 0.7,
|
| 166 |
+
"max_tokens": 50
|
| 167 |
+
}
|
| 168 |
+
print(f"Request: response_format={{'type': 'json_object'}}")
|
| 169 |
+
response = requests.post(
|
| 170 |
+
f"{API_BASE}/chat/completions",
|
| 171 |
+
json=payload,
|
| 172 |
+
timeout=120 # Increased timeout for model loading/generation
|
| 173 |
+
)
|
| 174 |
+
print(f"Status Code: {response.status_code}")
|
| 175 |
+
if response.status_code == 200:
|
| 176 |
+
data = response.json()
|
| 177 |
+
print(f"✅ response_format accepted")
|
| 178 |
+
if "choices" in data and len(data["choices"]) > 0:
|
| 179 |
+
content = data["choices"][0]["message"].get("content", "")
|
| 180 |
+
print(f"Generated content: {content}")
|
| 181 |
+
# Try to parse as JSON
|
| 182 |
+
try:
|
| 183 |
+
json.loads(content)
|
| 184 |
+
print("✅ Response is valid JSON")
|
| 185 |
+
return True
|
| 186 |
+
except json.JSONDecodeError:
|
| 187 |
+
print("⚠️ Response format requested but content is not JSON")
|
| 188 |
+
return False
|
| 189 |
+
return True
|
| 190 |
+
else:
|
| 191 |
+
print(f"❌ response_format test failed: {response.status_code}")
|
| 192 |
+
print(f"Response: {response.text}")
|
| 193 |
+
return False
|
| 194 |
+
except Exception as e:
|
| 195 |
+
print(f"❌ response_format test error: {e}")
|
| 196 |
+
import traceback
|
| 197 |
+
traceback.print_exc()
|
| 198 |
+
return False
|
| 199 |
+
|
| 200 |
+
def main():
|
| 201 |
+
"""Run all tests."""
|
| 202 |
+
print("\n" + "=" * 60)
|
| 203 |
+
print("BASIC SPACE API TESTS")
|
| 204 |
+
print("=" * 60)
|
| 205 |
+
print(f"Testing Space: {SPACE_URL}")
|
| 206 |
+
print(f"API Base: {API_BASE}")
|
| 207 |
+
print()
|
| 208 |
+
|
| 209 |
+
results = []
|
| 210 |
+
|
| 211 |
+
# Wait a bit for Space to be ready
|
| 212 |
+
print("Waiting 5 seconds for Space to be ready...")
|
| 213 |
+
time.sleep(5)
|
| 214 |
+
|
| 215 |
+
results.append(("Health Check", test_health()))
|
| 216 |
+
results.append(("List Models", test_list_models()))
|
| 217 |
+
results.append(("Simple Chat", test_simple_chat()))
|
| 218 |
+
results.append(("tool_choice='required'", test_tool_choice_required()))
|
| 219 |
+
results.append(("response_format", test_response_format()))
|
| 220 |
+
|
| 221 |
+
# Summary
|
| 222 |
+
print("\n" + "=" * 60)
|
| 223 |
+
print("TEST SUMMARY")
|
| 224 |
+
print("=" * 60)
|
| 225 |
+
passed = sum(1 for _, result in results if result)
|
| 226 |
+
total = len(results)
|
| 227 |
+
for test_name, result in results:
|
| 228 |
+
status = "✅ PASS" if result else "❌ FAIL"
|
| 229 |
+
print(f"{status}: {test_name}")
|
| 230 |
+
print(f"\nTotal: {passed}/{total} tests passed")
|
| 231 |
+
|
| 232 |
+
if passed == total:
|
| 233 |
+
print("\n🎉 All tests passed!")
|
| 234 |
+
else:
|
| 235 |
+
print(f"\n⚠️ {total - passed} test(s) failed")
|
| 236 |
+
|
| 237 |
+
return passed == total
|
| 238 |
+
|
| 239 |
+
if __name__ == "__main__":
|
| 240 |
+
success = main()
|
| 241 |
+
exit(0 if success else 1)
|
| 242 |
+
|
test_space_with_tools.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Test Space API with tool calls and JSON format."""
|
| 3 |
+
|
| 4 |
+
import requests
|
| 5 |
+
import json
|
| 6 |
+
import time
|
| 7 |
+
from typing import Dict, Any
|
| 8 |
+
|
| 9 |
+
SPACE_URL = "https://jeanbaptdzd-open-finance-llm-8b.hf.space"
|
| 10 |
+
API_BASE = f"{SPACE_URL}/v1"
|
| 11 |
+
|
| 12 |
+
def test_tool_calls():
|
| 13 |
+
"""Test tool calls functionality."""
|
| 14 |
+
print("=" * 60)
|
| 15 |
+
print("Test: Tool Calls")
|
| 16 |
+
print("=" * 60)
|
| 17 |
+
try:
|
| 18 |
+
payload = {
|
| 19 |
+
"model": "dragon-llm-open-finance",
|
| 20 |
+
"messages": [
|
| 21 |
+
{
|
| 22 |
+
"role": "user",
|
| 23 |
+
"content": "Calcule la valeur future de 10000€ à 5% sur 10 ans en utilisant l'outil calculer_valeur_future"
|
| 24 |
+
}
|
| 25 |
+
],
|
| 26 |
+
"tools": [
|
| 27 |
+
{
|
| 28 |
+
"type": "function",
|
| 29 |
+
"function": {
|
| 30 |
+
"name": "calculer_valeur_future",
|
| 31 |
+
"description": "Calcule la valeur future d'un capital avec intérêts composés",
|
| 32 |
+
"parameters": {
|
| 33 |
+
"type": "object",
|
| 34 |
+
"properties": {
|
| 35 |
+
"capital_initial": {
|
| 36 |
+
"type": "number",
|
| 37 |
+
"description": "Capital initial en euros"
|
| 38 |
+
},
|
| 39 |
+
"taux": {
|
| 40 |
+
"type": "number",
|
| 41 |
+
"description": "Taux d'intérêt annuel (ex: 0.05 pour 5%)"
|
| 42 |
+
},
|
| 43 |
+
"duree": {
|
| 44 |
+
"type": "number",
|
| 45 |
+
"description": "Durée en années"
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
"required": ["capital_initial", "taux", "duree"]
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
],
|
| 53 |
+
"tool_choice": "auto",
|
| 54 |
+
"temperature": 0.7,
|
| 55 |
+
"max_tokens": 200
|
| 56 |
+
}
|
| 57 |
+
print(f"Request: tool_choice='auto', tools provided")
|
| 58 |
+
response = requests.post(
|
| 59 |
+
f"{API_BASE}/chat/completions",
|
| 60 |
+
json=payload,
|
| 61 |
+
timeout=120
|
| 62 |
+
)
|
| 63 |
+
print(f"Status Code: {response.status_code}")
|
| 64 |
+
if response.status_code == 200:
|
| 65 |
+
data = response.json()
|
| 66 |
+
print(f"✅ Tool calls request accepted")
|
| 67 |
+
if "choices" in data and len(data["choices"]) > 0:
|
| 68 |
+
message = data["choices"][0]["message"]
|
| 69 |
+
if "tool_calls" in message and message["tool_calls"]:
|
| 70 |
+
print(f"✅ Tool calls found: {len(message['tool_calls'])}")
|
| 71 |
+
for i, tool_call in enumerate(message["tool_calls"]):
|
| 72 |
+
print(f" Tool call {i+1}:")
|
| 73 |
+
print(f" ID: {tool_call.get('id', 'N/A')}")
|
| 74 |
+
print(f" Function: {tool_call.get('function', {}).get('name', 'N/A')}")
|
| 75 |
+
print(f" Arguments: {tool_call.get('function', {}).get('arguments', 'N/A')[:100]}...")
|
| 76 |
+
return True
|
| 77 |
+
else:
|
| 78 |
+
print(f"⚠️ No tool_calls in response")
|
| 79 |
+
print(f" Content: {message.get('content', '')[:200]}...")
|
| 80 |
+
return False
|
| 81 |
+
return True
|
| 82 |
+
else:
|
| 83 |
+
print(f"❌ Tool calls test failed: {response.status_code}")
|
| 84 |
+
print(f"Response: {response.text}")
|
| 85 |
+
return False
|
| 86 |
+
except Exception as e:
|
| 87 |
+
print(f"❌ Tool calls test error: {e}")
|
| 88 |
+
import traceback
|
| 89 |
+
traceback.print_exc()
|
| 90 |
+
return False
|
| 91 |
+
|
| 92 |
+
def test_tool_choice_required():
|
| 93 |
+
"""Test tool_choice='required' with tools."""
|
| 94 |
+
print("\n" + "=" * 60)
|
| 95 |
+
print("Test: tool_choice='required' with Tools")
|
| 96 |
+
print("=" * 60)
|
| 97 |
+
try:
|
| 98 |
+
payload = {
|
| 99 |
+
"model": "dragon-llm-open-finance",
|
| 100 |
+
"messages": [
|
| 101 |
+
{
|
| 102 |
+
"role": "user",
|
| 103 |
+
"content": "Utilise l'outil pour calculer 10000€ à 5% sur 10 ans"
|
| 104 |
+
}
|
| 105 |
+
],
|
| 106 |
+
"tools": [
|
| 107 |
+
{
|
| 108 |
+
"type": "function",
|
| 109 |
+
"function": {
|
| 110 |
+
"name": "calculer_valeur_future",
|
| 111 |
+
"description": "Calcule la valeur future",
|
| 112 |
+
"parameters": {
|
| 113 |
+
"type": "object",
|
| 114 |
+
"properties": {
|
| 115 |
+
"capital_initial": {"type": "number"},
|
| 116 |
+
"taux": {"type": "number"},
|
| 117 |
+
"duree": {"type": "number"}
|
| 118 |
+
},
|
| 119 |
+
"required": ["capital_initial", "taux", "duree"]
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
],
|
| 124 |
+
"tool_choice": "required", # This should not cause 422 error
|
| 125 |
+
"temperature": 0.7,
|
| 126 |
+
"max_tokens": 200
|
| 127 |
+
}
|
| 128 |
+
print(f"Request: tool_choice='required'")
|
| 129 |
+
response = requests.post(
|
| 130 |
+
f"{API_BASE}/chat/completions",
|
| 131 |
+
json=payload,
|
| 132 |
+
timeout=120
|
| 133 |
+
)
|
| 134 |
+
print(f"Status Code: {response.status_code}")
|
| 135 |
+
if response.status_code == 200:
|
| 136 |
+
print(f"✅ tool_choice='required' accepted (no 422 error)")
|
| 137 |
+
return True
|
| 138 |
+
elif response.status_code == 422:
|
| 139 |
+
print(f"❌ Still getting 422 error with tool_choice='required'")
|
| 140 |
+
print(f"Response: {response.text}")
|
| 141 |
+
return False
|
| 142 |
+
else:
|
| 143 |
+
print(f"⚠️ Unexpected status: {response.status_code}")
|
| 144 |
+
return False
|
| 145 |
+
except Exception as e:
|
| 146 |
+
print(f"❌ Error: {e}")
|
| 147 |
+
return False
|
| 148 |
+
|
| 149 |
+
def test_response_format_json():
|
| 150 |
+
"""Test response_format with JSON output."""
|
| 151 |
+
print("\n" + "=" * 60)
|
| 152 |
+
print("Test: response_format JSON")
|
| 153 |
+
print("=" * 60)
|
| 154 |
+
try:
|
| 155 |
+
payload = {
|
| 156 |
+
"model": "dragon-llm-open-finance",
|
| 157 |
+
"messages": [
|
| 158 |
+
{
|
| 159 |
+
"role": "user",
|
| 160 |
+
"content": "Donne-moi un nombre aléatoire entre 1 et 10 au format JSON avec la clé 'nombre'"
|
| 161 |
+
}
|
| 162 |
+
],
|
| 163 |
+
"response_format": {"type": "json_object"},
|
| 164 |
+
"temperature": 0.7,
|
| 165 |
+
"max_tokens": 100
|
| 166 |
+
}
|
| 167 |
+
print(f"Request: response_format={{'type': 'json_object'}}")
|
| 168 |
+
response = requests.post(
|
| 169 |
+
f"{API_BASE}/chat/completions",
|
| 170 |
+
json=payload,
|
| 171 |
+
timeout=120
|
| 172 |
+
)
|
| 173 |
+
print(f"Status Code: {response.status_code}")
|
| 174 |
+
if response.status_code == 200:
|
| 175 |
+
data = response.json()
|
| 176 |
+
print(f"✅ response_format accepted")
|
| 177 |
+
if "choices" in data and len(data["choices"]) > 0:
|
| 178 |
+
content = data["choices"][0]["message"].get("content", "")
|
| 179 |
+
print(f"Generated content (first 200 chars): {content[:200]}")
|
| 180 |
+
|
| 181 |
+
# Try to parse as JSON
|
| 182 |
+
try:
|
| 183 |
+
# Remove reasoning tags if present
|
| 184 |
+
cleaned = content
|
| 185 |
+
if "<think>" in cleaned.lower():
|
| 186 |
+
# Try to extract JSON after reasoning
|
| 187 |
+
if "}" in cleaned:
|
| 188 |
+
brace_pos = cleaned.find('{')
|
| 189 |
+
if brace_pos != -1:
|
| 190 |
+
cleaned = cleaned[brace_pos:]
|
| 191 |
+
|
| 192 |
+
json_data = json.loads(cleaned)
|
| 193 |
+
print(f"✅ Response is valid JSON: {json_data}")
|
| 194 |
+
return True
|
| 195 |
+
except json.JSONDecodeError as e:
|
| 196 |
+
print(f"⚠️ Response is not valid JSON: {e}")
|
| 197 |
+
print(f" Full content: {content[:500]}")
|
| 198 |
+
return False
|
| 199 |
+
return True
|
| 200 |
+
else:
|
| 201 |
+
print(f"❌ response_format test failed: {response.status_code}")
|
| 202 |
+
print(f"Response: {response.text}")
|
| 203 |
+
return False
|
| 204 |
+
except Exception as e:
|
| 205 |
+
print(f"❌ Error: {e}")
|
| 206 |
+
import traceback
|
| 207 |
+
traceback.print_exc()
|
| 208 |
+
return False
|
| 209 |
+
|
| 210 |
+
def main():
|
| 211 |
+
"""Run all tests."""
|
| 212 |
+
print("\n" + "=" * 60)
|
| 213 |
+
print("SPACE API TESTS - TOOLS AND JSON FORMAT")
|
| 214 |
+
print("=" * 60)
|
| 215 |
+
print(f"Testing Space: {SPACE_URL}")
|
| 216 |
+
print()
|
| 217 |
+
|
| 218 |
+
results = []
|
| 219 |
+
|
| 220 |
+
results.append(("Tool Calls (auto)", test_tool_calls()))
|
| 221 |
+
results.append(("tool_choice='required'", test_tool_choice_required()))
|
| 222 |
+
results.append(("response_format JSON", test_response_format_json()))
|
| 223 |
+
|
| 224 |
+
# Summary
|
| 225 |
+
print("\n" + "=" * 60)
|
| 226 |
+
print("TEST SUMMARY")
|
| 227 |
+
print("=" * 60)
|
| 228 |
+
passed = sum(1 for _, result in results if result)
|
| 229 |
+
total = len(results)
|
| 230 |
+
for test_name, result in results:
|
| 231 |
+
status = "✅ PASS" if result else "❌ FAIL"
|
| 232 |
+
print(f"{status}: {test_name}")
|
| 233 |
+
print(f"\nTotal: {passed}/{total} tests passed")
|
| 234 |
+
|
| 235 |
+
return passed == total
|
| 236 |
+
|
| 237 |
+
if __name__ == "__main__":
|
| 238 |
+
success = main()
|
| 239 |
+
exit(0 if success else 1)
|
| 240 |
+
|