jeanbaptdzd commited on
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 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
- - ✅ **PydanticAI Integration** - High-level agent framework included
 
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
+