aiforge-esprit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiforge-esprit
3
+ Version: 0.1.0
4
+ Summary: SDK Python officiel d'AI Forge ESPRIT — un enrobage fin du SDK OpenAI.
5
+ Author: Direction IA - ESPRIT
6
+ License: MIT
7
+ Project-URL: Homepage, https://aiforge.esprit.tn
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: openai>=1.40
11
+ Requires-Dist: pydantic>=2
12
+
13
+ # aiforge
14
+
15
+ *The official Python SDK for AI Forge ESPRIT — a thin wrapper around the OpenAI SDK.*
16
+
17
+ `aiforge` enrobe le SDK OpenAI officiel : il ne réimplémente jamais le HTTP, il
18
+ ajoute juste quatre macros et la facturation en TND.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install aiforge-esprit
24
+ ```
25
+
26
+ ## Démarrage rapide
27
+
28
+ ```python
29
+ from aiforge import AIForge
30
+
31
+ af = AIForge() # clé depuis AIFORGE_API_KEY
32
+ print(af.chat("Explique les transformers en 2 phrases."))
33
+ print(af.code("Écris un quicksort en Python"))
34
+ print(af.balance())
35
+ ```
36
+
37
+ La clé vient de l'argument `api_key=` ou de la variable `AIFORGE_API_KEY`.
38
+ L'URL vient de `base_url=` ou de `AIFORGE_BASE_URL` (défaut :
39
+ `https://aiforge.esprit.tn/api/v1`).
40
+
41
+ ## Les trois niveaux de contrôle du modèle
42
+
43
+ | Niveau | Exemple | Coût de routage |
44
+ |---|---|---|
45
+ | **Nom exact** | `chat(..., model="Qwen3.6-27B")` | aucun |
46
+ | **Alias statique** | `chat(..., model="coder")` | aucun |
47
+ | **`auto`** (défaut) | `chat(...)` | un appel classifieur |
48
+
49
+ En `auto`, un classifieur choisit le spécialiste selon la demande (et route vers
50
+ le modèle vision si une image est présente). Le champ `model` de la réponse porte
51
+ toujours le **modèle réellement utilisé**.
52
+
53
+ ### Alias
54
+
55
+ | Alias | Modèle |
56
+ |---|---|
57
+ | `fast` | SmolLM3-3B |
58
+ | `smart` | Qwen3.6-35B-A3B |
59
+ | `coder` | Qwen3.6-27B |
60
+ | `vision` | Gemma-3-4B-IT |
61
+
62
+ ## Les 4 macros
63
+
64
+ ```python
65
+ af.chat(prompt, model="auto", system=None, max_tokens=2000, stream=False) # -> str (ou générateur si stream=True)
66
+ af.code(prompt) # chat model="coder", max_tokens=3000
67
+ af.extract(text, schema) # schema = classe pydantic ou dict json-schema -> instance validée
68
+ af.vision(image, prompt) # image = chemin local, bytes ou URL http(s)
69
+ af.balance() # -> {"balanceTND", "monthlyBudgetTND", "currency"}
70
+ ```
71
+
72
+ Pour `extract`, le schéma garantit la forme ; le system prompt intégré impose la
73
+ concision (valeurs seules, sans phrases) ; pour des extractions complexes,
74
+ préférez `model="smart"`.
75
+
76
+ Avec `AIForge(verbose=True)`, chaque appel imprime une ligne :
77
+ `-> {modèle final} · {prompt}+{completion} tokens · {coût} TND`.
78
+
79
+ ## Modèles à raisonnement (smart, coder)
80
+
81
+ `Qwen3.6-27B` et `Qwen3.6-35B-A3B` **réfléchissent avant de répondre** : la
82
+ réflexion arrive dans le champ `reasoning` de la réponse (nom vLLM 0.25). Un
83
+ `max_tokens` trop bas peut être entièrement consommé par la réflexion — le SDK
84
+ lève alors une erreur claire. **Utilisez `max_tokens >= 1500` pour `smart` et
85
+ `coder`.**
86
+
87
+ ## Note de facturation
88
+
89
+ En mode `auto`, le coût du **classifieur** est imputé côté serveur (une ligne
90
+ d'usage distincte) mais **n'apparaît pas dans la réponse** — la ligne `verbose`
91
+ n'affiche donc que le coût du modèle final. Consultez la page Usage pour le
92
+ détail complet.
93
+
94
+ ## Licence
95
+
96
+ MIT — Direction IA - ESPRIT.
@@ -0,0 +1,84 @@
1
+ # aiforge
2
+
3
+ *The official Python SDK for AI Forge ESPRIT — a thin wrapper around the OpenAI SDK.*
4
+
5
+ `aiforge` enrobe le SDK OpenAI officiel : il ne réimplémente jamais le HTTP, il
6
+ ajoute juste quatre macros et la facturation en TND.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pip install aiforge-esprit
12
+ ```
13
+
14
+ ## Démarrage rapide
15
+
16
+ ```python
17
+ from aiforge import AIForge
18
+
19
+ af = AIForge() # clé depuis AIFORGE_API_KEY
20
+ print(af.chat("Explique les transformers en 2 phrases."))
21
+ print(af.code("Écris un quicksort en Python"))
22
+ print(af.balance())
23
+ ```
24
+
25
+ La clé vient de l'argument `api_key=` ou de la variable `AIFORGE_API_KEY`.
26
+ L'URL vient de `base_url=` ou de `AIFORGE_BASE_URL` (défaut :
27
+ `https://aiforge.esprit.tn/api/v1`).
28
+
29
+ ## Les trois niveaux de contrôle du modèle
30
+
31
+ | Niveau | Exemple | Coût de routage |
32
+ |---|---|---|
33
+ | **Nom exact** | `chat(..., model="Qwen3.6-27B")` | aucun |
34
+ | **Alias statique** | `chat(..., model="coder")` | aucun |
35
+ | **`auto`** (défaut) | `chat(...)` | un appel classifieur |
36
+
37
+ En `auto`, un classifieur choisit le spécialiste selon la demande (et route vers
38
+ le modèle vision si une image est présente). Le champ `model` de la réponse porte
39
+ toujours le **modèle réellement utilisé**.
40
+
41
+ ### Alias
42
+
43
+ | Alias | Modèle |
44
+ |---|---|
45
+ | `fast` | SmolLM3-3B |
46
+ | `smart` | Qwen3.6-35B-A3B |
47
+ | `coder` | Qwen3.6-27B |
48
+ | `vision` | Gemma-3-4B-IT |
49
+
50
+ ## Les 4 macros
51
+
52
+ ```python
53
+ af.chat(prompt, model="auto", system=None, max_tokens=2000, stream=False) # -> str (ou générateur si stream=True)
54
+ af.code(prompt) # chat model="coder", max_tokens=3000
55
+ af.extract(text, schema) # schema = classe pydantic ou dict json-schema -> instance validée
56
+ af.vision(image, prompt) # image = chemin local, bytes ou URL http(s)
57
+ af.balance() # -> {"balanceTND", "monthlyBudgetTND", "currency"}
58
+ ```
59
+
60
+ Pour `extract`, le schéma garantit la forme ; le system prompt intégré impose la
61
+ concision (valeurs seules, sans phrases) ; pour des extractions complexes,
62
+ préférez `model="smart"`.
63
+
64
+ Avec `AIForge(verbose=True)`, chaque appel imprime une ligne :
65
+ `-> {modèle final} · {prompt}+{completion} tokens · {coût} TND`.
66
+
67
+ ## Modèles à raisonnement (smart, coder)
68
+
69
+ `Qwen3.6-27B` et `Qwen3.6-35B-A3B` **réfléchissent avant de répondre** : la
70
+ réflexion arrive dans le champ `reasoning` de la réponse (nom vLLM 0.25). Un
71
+ `max_tokens` trop bas peut être entièrement consommé par la réflexion — le SDK
72
+ lève alors une erreur claire. **Utilisez `max_tokens >= 1500` pour `smart` et
73
+ `coder`.**
74
+
75
+ ## Note de facturation
76
+
77
+ En mode `auto`, le coût du **classifieur** est imputé côté serveur (une ligne
78
+ d'usage distincte) mais **n'apparaît pas dans la réponse** — la ligne `verbose`
79
+ n'affiche donc que le coût du modèle final. Consultez la page Usage pour le
80
+ détail complet.
81
+
82
+ ## Licence
83
+
84
+ MIT — Direction IA - ESPRIT.
@@ -0,0 +1,4 @@
1
+ from .client import AIForge, AIForgeError
2
+
3
+ __version__ = "0.1.0"
4
+ __all__ = ["AIForge", "AIForgeError", "__version__"]
@@ -0,0 +1,302 @@
1
+ """AIForge — a thin wrapper around the official OpenAI SDK. HTTP is never
2
+ reimplemented: chat/completions go through `openai`, and the two auxiliary GETs
3
+ (balance, tarifs) reuse httpx, which openai already depends on."""
4
+ from __future__ import annotations
5
+
6
+ import base64
7
+ import json
8
+ import mimetypes
9
+ import os
10
+ from typing import Any, Dict, Iterator, Optional, Type, Union
11
+
12
+ import httpx
13
+ import openai
14
+ from pydantic import BaseModel, ValidationError
15
+
16
+ DEFAULT_BASE_URL = "https://aiforge.esprit.tn/api/v1"
17
+
18
+ # Modèles virtuels côté serveur : "auto" (routeur dynamique) + alias statiques.
19
+ AUTO_MODEL = "auto"
20
+ ALIASES = {"fast", "smart", "coder", "vision"}
21
+
22
+
23
+ class AIForgeError(Exception):
24
+ """Erreur AIForge, message en français prêt à afficher."""
25
+
26
+
27
+ class AIForge:
28
+ def __init__(
29
+ self,
30
+ api_key: Optional[str] = None,
31
+ base_url: Optional[str] = None,
32
+ verbose: bool = False,
33
+ timeout: float = 120,
34
+ ) -> None:
35
+ api_key = api_key or os.environ.get("AIFORGE_API_KEY")
36
+ if not api_key:
37
+ raise AIForgeError(
38
+ "Clé API manquante. Passez api_key=... ou définissez la variable "
39
+ "d'environnement AIFORGE_API_KEY (votre clé « tf-... »)."
40
+ )
41
+ self.api_key = api_key
42
+ self.base_url = (base_url or os.environ.get("AIFORGE_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
43
+ self.verbose = verbose
44
+ self.timeout = timeout
45
+ self.client = openai.OpenAI(base_url=self.base_url, api_key=self.api_key, timeout=timeout)
46
+ # Cache paresseux des tarifs {nom_modèle: (inputPriceTND, outputPriceTND)}.
47
+ self._prices: Optional[Dict[str, tuple]] = None
48
+
49
+ # ── Tarifs (cache paresseux via /api/public/models) ──────────────────────
50
+
51
+ def _prices_url(self) -> Optional[str]:
52
+ if "/api/v1" in self.base_url:
53
+ return self.base_url.split("/api/v1")[0] + "/api/public/models"
54
+ return None
55
+
56
+ def _load_prices(self) -> Dict[str, tuple]:
57
+ if self._prices is not None:
58
+ return self._prices
59
+ self._prices = {}
60
+ url = self._prices_url()
61
+ if not url:
62
+ return self._prices
63
+ try:
64
+ r = httpx.get(url, timeout=self.timeout)
65
+ r.raise_for_status()
66
+ for m in r.json().get("models", []):
67
+ if "inputPriceTND" in m and "outputPriceTND" in m:
68
+ self._prices[m["name"]] = (m["inputPriceTND"], m["outputPriceTND"])
69
+ except Exception:
70
+ # Tarifs indisponibles → coûts affichés en "?" plutôt qu'inventés.
71
+ pass
72
+ return self._prices
73
+
74
+ def _cost_tnd(self, model: str, prompt_tokens: int, completion_tokens: int) -> Optional[float]:
75
+ prices = self._load_prices()
76
+ if model not in prices:
77
+ return None
78
+ pin, pout = prices[model]
79
+ return prompt_tokens / 1_000_000 * pin + completion_tokens / 1_000_000 * pout
80
+
81
+ def _log(self, requested: str, final_model: str, usage: Any) -> None:
82
+ prompt = getattr(usage, "prompt_tokens", 0) if usage else 0
83
+ completion = getattr(usage, "completion_tokens", 0) if usage else 0
84
+ label = f"{requested} -> {final_model}" if (requested == AUTO_MODEL or requested in ALIASES) else final_model
85
+ cost = self._cost_tnd(final_model, prompt, completion)
86
+ cost_str = f"{cost:.3f} TND" if cost is not None else "? TND"
87
+ print(f"-> {label} · {prompt}+{completion} tokens · {cost_str}")
88
+
89
+ # ── Traduction des erreurs ───────────────────────────────────────────────
90
+
91
+ @staticmethod
92
+ def _translate_error(e: Exception) -> AIForgeError:
93
+ status = getattr(e, "status_code", None)
94
+ if status == 401:
95
+ return AIForgeError("Clé API invalide ou manquante (401).")
96
+ if status == 402:
97
+ return AIForgeError("Budget épuisé (402). Il est renouvelé le 1er du mois.")
98
+ if status == 403:
99
+ return AIForgeError("Modèle non autorisé pour votre organisation (403).")
100
+ if isinstance(e, openai.APIConnectionError):
101
+ return AIForgeError(f"Connexion impossible à AIForge : {e}")
102
+ return AIForgeError(str(e))
103
+
104
+ # ── Macros ────────────────────────────────────────────────────────────────
105
+
106
+ def chat(
107
+ self,
108
+ prompt: str,
109
+ model: str = "auto",
110
+ system: Optional[str] = None,
111
+ max_tokens: int = 2000,
112
+ temperature: Optional[float] = None,
113
+ stream: bool = False,
114
+ **kwargs: Any,
115
+ ) -> Union[str, Iterator[str]]:
116
+ messages = []
117
+ if system:
118
+ messages.append({"role": "system", "content": system})
119
+ messages.append({"role": "user", "content": prompt})
120
+
121
+ params: Dict[str, Any] = {"model": model, "messages": messages, "max_tokens": max_tokens}
122
+ if temperature is not None:
123
+ params["temperature"] = temperature
124
+ params.update(kwargs)
125
+
126
+ if stream:
127
+ return self._chat_stream(params, model)
128
+
129
+ try:
130
+ resp = self.client.chat.completions.create(**params)
131
+ except openai.APIStatusError as e:
132
+ raise self._translate_error(e)
133
+ except openai.APIConnectionError as e:
134
+ raise self._translate_error(e)
135
+
136
+ choice = resp.choices[0]
137
+ content = choice.message.content
138
+ # Piège reasoning : la réflexion a tout consommé, aucune réponse produite.
139
+ if choice.finish_reason == "length" and not content:
140
+ raise AIForgeError(
141
+ "max_tokens épuisé par la réflexion du modèle, sans réponse produite. "
142
+ "Augmentez max_tokens (>= 1500 pour smart/coder — ces modèles "
143
+ "réfléchissent avant de répondre)."
144
+ )
145
+ if self.verbose:
146
+ self._log(model, resp.model, resp.usage)
147
+ return content or ""
148
+
149
+ def _chat_stream(self, params: Dict[str, Any], requested: str) -> Iterator[str]:
150
+ params = dict(params, stream=True)
151
+ params.setdefault("stream_options", {"include_usage": True})
152
+ try:
153
+ stream = self.client.chat.completions.create(**params)
154
+ except openai.APIStatusError as e:
155
+ raise self._translate_error(e)
156
+ except openai.APIConnectionError as e:
157
+ raise self._translate_error(e)
158
+
159
+ usage = None
160
+ final_model = requested
161
+ for chunk in stream:
162
+ if getattr(chunk, "usage", None):
163
+ usage = chunk.usage
164
+ if getattr(chunk, "model", None):
165
+ final_model = chunk.model
166
+ if not chunk.choices:
167
+ continue
168
+ delta = chunk.choices[0].delta
169
+ content = getattr(delta, "content", None)
170
+ if content: # deltas reasoning (content None) filtrés
171
+ yield content
172
+ if self.verbose:
173
+ self._log(requested, final_model, usage)
174
+
175
+ def code(self, prompt: str, **kwargs: Any) -> Union[str, Iterator[str]]:
176
+ kwargs.setdefault("model", "coder")
177
+ kwargs.setdefault("max_tokens", 3000)
178
+ return self.chat(prompt, **kwargs)
179
+
180
+ def extract(
181
+ self,
182
+ text: str,
183
+ schema: Union[Type[BaseModel], Dict[str, Any]],
184
+ model: str = "fast",
185
+ max_tokens: int = 2000,
186
+ **kwargs: Any,
187
+ ) -> Union[BaseModel, Dict[str, Any]]:
188
+ is_pydantic = isinstance(schema, type) and issubclass(schema, BaseModel)
189
+ if is_pydantic:
190
+ schema_dict = schema.model_json_schema()
191
+ name = schema.__name__
192
+ else:
193
+ schema_dict = schema
194
+ name = "extraction"
195
+
196
+ response_format = {
197
+ "type": "json_schema",
198
+ "json_schema": {"name": name, "schema": schema_dict},
199
+ }
200
+ # Le schéma contraint la structure, pas la concision : un system ferme
201
+ # impose des valeurs seules (sans lui, SmolLM remplit les champs de prose).
202
+ messages = []
203
+ caller_system = kwargs.pop("system", None)
204
+ if caller_system: # le system de l'appelant précède le nôtre
205
+ messages.append({"role": "system", "content": caller_system})
206
+ messages.append({
207
+ "role": "system",
208
+ "content": (
209
+ "Tu es un extracteur de données. Remplis chaque champ du schéma avec la "
210
+ "valeur exacte extraite, la plus courte possible. Aucune explication, "
211
+ "aucune phrase, aucun commentaire - uniquement les valeurs."
212
+ ),
213
+ })
214
+ messages.append({"role": "user", "content": text})
215
+ try:
216
+ resp = self.client.chat.completions.create(
217
+ model=model,
218
+ messages=messages,
219
+ max_tokens=max_tokens,
220
+ response_format=response_format,
221
+ **kwargs,
222
+ )
223
+ except openai.APIStatusError as e:
224
+ raise self._translate_error(e)
225
+ except openai.APIConnectionError as e:
226
+ raise self._translate_error(e)
227
+
228
+ choice = resp.choices[0]
229
+ content = choice.message.content
230
+ if choice.finish_reason == "length" and not content:
231
+ raise AIForgeError(
232
+ "max_tokens épuisé avant de produire le JSON. Augmentez max_tokens."
233
+ )
234
+ if self.verbose:
235
+ self._log(model, resp.model, resp.usage)
236
+
237
+ if is_pydantic:
238
+ try:
239
+ return schema.model_validate_json(content or "")
240
+ except ValidationError as e:
241
+ raise AIForgeError(f"Le JSON renvoyé ne respecte pas le schéma : {e}")
242
+ try:
243
+ return json.loads(content or "")
244
+ except json.JSONDecodeError as e:
245
+ raise AIForgeError(f"Réponse non-JSON du modèle : {e}")
246
+
247
+ def vision(
248
+ self,
249
+ image: Union[str, bytes],
250
+ prompt: str,
251
+ model: str = "vision",
252
+ max_tokens: int = 2000,
253
+ ) -> str:
254
+ url = self._image_to_url(image)
255
+ content = [
256
+ {"type": "text", "text": prompt},
257
+ {"type": "image_url", "image_url": {"url": url}},
258
+ ]
259
+ try:
260
+ resp = self.client.chat.completions.create(
261
+ model=model,
262
+ messages=[{"role": "user", "content": content}],
263
+ max_tokens=max_tokens,
264
+ )
265
+ except openai.APIStatusError as e:
266
+ raise self._translate_error(e)
267
+ except openai.APIConnectionError as e:
268
+ raise self._translate_error(e)
269
+
270
+ choice = resp.choices[0]
271
+ out = choice.message.content
272
+ if choice.finish_reason == "length" and not out:
273
+ raise AIForgeError("max_tokens épuisé avant de produire une réponse. Augmentez max_tokens.")
274
+ if self.verbose:
275
+ self._log(model, resp.model, resp.usage)
276
+ return out or ""
277
+
278
+ @staticmethod
279
+ def _image_to_url(image: Union[str, bytes]) -> str:
280
+ if isinstance(image, str) and (image.startswith("http://") or image.startswith("https://")):
281
+ return image
282
+ if isinstance(image, bytes):
283
+ data = image
284
+ mime = "image/jpeg"
285
+ else: # chemin local
286
+ with open(image, "rb") as f:
287
+ data = f.read()
288
+ mime = mimetypes.guess_type(image)[0] or "image/jpeg"
289
+ b64 = base64.b64encode(data).decode("ascii")
290
+ return f"data:{mime};base64,{b64}"
291
+
292
+ def balance(self) -> Dict[str, Any]:
293
+ url = self.base_url + "/balance"
294
+ try:
295
+ r = httpx.get(url, headers={"Authorization": f"Bearer {self.api_key}"}, timeout=self.timeout)
296
+ except httpx.HTTPError as e:
297
+ raise AIForgeError(f"Connexion impossible à AIForge : {e}")
298
+ if r.status_code == 401:
299
+ raise AIForgeError("Clé API invalide ou manquante (401).")
300
+ if r.status_code >= 400:
301
+ raise AIForgeError(f"Erreur solde ({r.status_code}) : {r.text}")
302
+ return r.json()
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiforge-esprit
3
+ Version: 0.1.0
4
+ Summary: SDK Python officiel d'AI Forge ESPRIT — un enrobage fin du SDK OpenAI.
5
+ Author: Direction IA - ESPRIT
6
+ License: MIT
7
+ Project-URL: Homepage, https://aiforge.esprit.tn
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: openai>=1.40
11
+ Requires-Dist: pydantic>=2
12
+
13
+ # aiforge
14
+
15
+ *The official Python SDK for AI Forge ESPRIT — a thin wrapper around the OpenAI SDK.*
16
+
17
+ `aiforge` enrobe le SDK OpenAI officiel : il ne réimplémente jamais le HTTP, il
18
+ ajoute juste quatre macros et la facturation en TND.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install aiforge-esprit
24
+ ```
25
+
26
+ ## Démarrage rapide
27
+
28
+ ```python
29
+ from aiforge import AIForge
30
+
31
+ af = AIForge() # clé depuis AIFORGE_API_KEY
32
+ print(af.chat("Explique les transformers en 2 phrases."))
33
+ print(af.code("Écris un quicksort en Python"))
34
+ print(af.balance())
35
+ ```
36
+
37
+ La clé vient de l'argument `api_key=` ou de la variable `AIFORGE_API_KEY`.
38
+ L'URL vient de `base_url=` ou de `AIFORGE_BASE_URL` (défaut :
39
+ `https://aiforge.esprit.tn/api/v1`).
40
+
41
+ ## Les trois niveaux de contrôle du modèle
42
+
43
+ | Niveau | Exemple | Coût de routage |
44
+ |---|---|---|
45
+ | **Nom exact** | `chat(..., model="Qwen3.6-27B")` | aucun |
46
+ | **Alias statique** | `chat(..., model="coder")` | aucun |
47
+ | **`auto`** (défaut) | `chat(...)` | un appel classifieur |
48
+
49
+ En `auto`, un classifieur choisit le spécialiste selon la demande (et route vers
50
+ le modèle vision si une image est présente). Le champ `model` de la réponse porte
51
+ toujours le **modèle réellement utilisé**.
52
+
53
+ ### Alias
54
+
55
+ | Alias | Modèle |
56
+ |---|---|
57
+ | `fast` | SmolLM3-3B |
58
+ | `smart` | Qwen3.6-35B-A3B |
59
+ | `coder` | Qwen3.6-27B |
60
+ | `vision` | Gemma-3-4B-IT |
61
+
62
+ ## Les 4 macros
63
+
64
+ ```python
65
+ af.chat(prompt, model="auto", system=None, max_tokens=2000, stream=False) # -> str (ou générateur si stream=True)
66
+ af.code(prompt) # chat model="coder", max_tokens=3000
67
+ af.extract(text, schema) # schema = classe pydantic ou dict json-schema -> instance validée
68
+ af.vision(image, prompt) # image = chemin local, bytes ou URL http(s)
69
+ af.balance() # -> {"balanceTND", "monthlyBudgetTND", "currency"}
70
+ ```
71
+
72
+ Pour `extract`, le schéma garantit la forme ; le system prompt intégré impose la
73
+ concision (valeurs seules, sans phrases) ; pour des extractions complexes,
74
+ préférez `model="smart"`.
75
+
76
+ Avec `AIForge(verbose=True)`, chaque appel imprime une ligne :
77
+ `-> {modèle final} · {prompt}+{completion} tokens · {coût} TND`.
78
+
79
+ ## Modèles à raisonnement (smart, coder)
80
+
81
+ `Qwen3.6-27B` et `Qwen3.6-35B-A3B` **réfléchissent avant de répondre** : la
82
+ réflexion arrive dans le champ `reasoning` de la réponse (nom vLLM 0.25). Un
83
+ `max_tokens` trop bas peut être entièrement consommé par la réflexion — le SDK
84
+ lève alors une erreur claire. **Utilisez `max_tokens >= 1500` pour `smart` et
85
+ `coder`.**
86
+
87
+ ## Note de facturation
88
+
89
+ En mode `auto`, le coût du **classifieur** est imputé côté serveur (une ligne
90
+ d'usage distincte) mais **n'apparaît pas dans la réponse** — la ligne `verbose`
91
+ n'affiche donc que le coût du modèle final. Consultez la page Usage pour le
92
+ détail complet.
93
+
94
+ ## Licence
95
+
96
+ MIT — Direction IA - ESPRIT.
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ aiforge/__init__.py
4
+ aiforge/client.py
5
+ aiforge_esprit.egg-info/PKG-INFO
6
+ aiforge_esprit.egg-info/SOURCES.txt
7
+ aiforge_esprit.egg-info/dependency_links.txt
8
+ aiforge_esprit.egg-info/requires.txt
9
+ aiforge_esprit.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ openai>=1.40
2
+ pydantic>=2
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "aiforge-esprit"
7
+ version = "0.1.0"
8
+ description = "SDK Python officiel d'AI Forge ESPRIT — un enrobage fin du SDK OpenAI."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Direction IA - ESPRIT" }]
12
+ requires-python = ">=3.9"
13
+ dependencies = ["openai>=1.40", "pydantic>=2"]
14
+
15
+ [project.urls]
16
+ Homepage = "https://aiforge.esprit.tn"
17
+
18
+ [tool.setuptools.packages.find]
19
+ where = ["."]
20
+ include = ["aiforge*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+