Cyril Dupland commited on
Commit ·
c392583
1
Parent(s): 6c4dfd3
Refactoring gestion Agent
Browse files- README.md +4 -3
- api/routes/completion.py +4 -4
- docs/API_EXAMPLES.md +20 -15
- domain/models.py +7 -4
- graphs/README.md +0 -63
- graphs/base_graph.py +0 -193
- graphs/workflows/conversation.py +0 -24
- graphs/workflows/{conversation_with_summary.py → orchestrated_v2.py} +42 -10
- postman_collection.json +5 -5
- services/agent_registry.py +41 -60
- services/agent_service.py +12 -10
- services/voice/voice_agent_service.py +2 -2
- services/voice/voice_pipeline.py +4 -4
README.md
CHANGED
|
@@ -157,7 +157,7 @@ curl -X POST "http://localhost:7860/completion" \
|
|
| 157 |
-d '{
|
| 158 |
"message": "Bonjour, comment vas-tu?",
|
| 159 |
"model": "gpt-4o",
|
| 160 |
-
"
|
| 161 |
"stream": false,
|
| 162 |
"temperature": 0.7
|
| 163 |
}'
|
|
@@ -333,13 +333,13 @@ def create_custom_graph(llm):
|
|
| 333 |
from graphs.custom_graph import create_custom_graph
|
| 334 |
|
| 335 |
agent_registry.register_agent(
|
| 336 |
-
|
| 337 |
create_custom_graph,
|
| 338 |
"Description de votre agent"
|
| 339 |
)
|
| 340 |
```
|
| 341 |
|
| 342 |
-
3. Utilisez-le via l'API
|
| 343 |
|
| 344 |
## 🧪 Tests
|
| 345 |
|
|
@@ -370,6 +370,7 @@ LANGCHAIN_PROJECT=routeur-ia
|
|
| 370 |
|
| 371 |
## 📝 TODO / Roadmap
|
| 372 |
|
|
|
|
| 373 |
- [ ] Tests unitaires et d'intégration
|
| 374 |
- [ ] Implémentation complète WebRTC avec aiortc
|
| 375 |
- [ ] Agent RAG avec base vectorielle
|
|
|
|
| 157 |
-d '{
|
| 158 |
"message": "Bonjour, comment vas-tu?",
|
| 159 |
"model": "gpt-4o",
|
| 160 |
+
"agent": "V2",
|
| 161 |
"stream": false,
|
| 162 |
"temperature": 0.7
|
| 163 |
}'
|
|
|
|
| 333 |
from graphs.custom_graph import create_custom_graph
|
| 334 |
|
| 335 |
agent_registry.register_agent(
|
| 336 |
+
"my_agent",
|
| 337 |
create_custom_graph,
|
| 338 |
"Description de votre agent"
|
| 339 |
)
|
| 340 |
```
|
| 341 |
|
| 342 |
+
3. Utilisez-le via l'API avec `"agent": "my_agent"` dans la requête `POST /completion`.
|
| 343 |
|
| 344 |
## 🧪 Tests
|
| 345 |
|
|
|
|
| 370 |
|
| 371 |
## 📝 TODO / Roadmap
|
| 372 |
|
| 373 |
+
- [ ] Harmoniser la nomenclature du pipeline voix: le champ `agent_type` est encore utilisé dans `services/voice/voice_pipeline.py` pour des événements internes et devra être renommé en `agent` lors d'un prochain refactoring.
|
| 374 |
- [ ] Tests unitaires et d'intégration
|
| 375 |
- [ ] Implémentation complète WebRTC avec aiortc
|
| 376 |
- [ ] Agent RAG avec base vectorielle
|
api/routes/completion.py
CHANGED
|
@@ -61,7 +61,7 @@ async def complete(
|
|
| 61 |
- Content-Type: `text/event-stream`
|
| 62 |
|
| 63 |
Args:
|
| 64 |
-
request: Completion request with message, model, agent
|
| 65 |
current_user: Authenticated user (JWT required)
|
| 66 |
|
| 67 |
Returns:
|
|
@@ -86,7 +86,7 @@ async def complete(
|
|
| 86 |
return await _complete(request)
|
| 87 |
|
| 88 |
except ValueError as e:
|
| 89 |
-
# Agent
|
| 90 |
raise HTTPException(
|
| 91 |
status_code=status.HTTP_400_BAD_REQUEST,
|
| 92 |
detail=str(e)
|
|
@@ -112,7 +112,7 @@ async def _complete(request: CompletionRequest) -> CompletionResponse:
|
|
| 112 |
result = await agent_service.invoke(
|
| 113 |
message=request.message,
|
| 114 |
model_name=request.model,
|
| 115 |
-
|
| 116 |
temperature=request.temperature,
|
| 117 |
max_tokens=request.max_tokens,
|
| 118 |
conversation_history=request.conversation_history,
|
|
@@ -139,7 +139,7 @@ async def _stream_completion(request: CompletionRequest) -> StreamingResponse:
|
|
| 139 |
async for chunk in agent_service.stream(
|
| 140 |
message=request.message,
|
| 141 |
model_name=request.model,
|
| 142 |
-
|
| 143 |
temperature=request.temperature,
|
| 144 |
max_tokens=request.max_tokens,
|
| 145 |
conversation_history=request.conversation_history,
|
|
|
|
| 61 |
- Content-Type: `text/event-stream`
|
| 62 |
|
| 63 |
Args:
|
| 64 |
+
request: Completion request with message, model, agent and streaming flag
|
| 65 |
current_user: Authenticated user (JWT required)
|
| 66 |
|
| 67 |
Returns:
|
|
|
|
| 86 |
return await _complete(request)
|
| 87 |
|
| 88 |
except ValueError as e:
|
| 89 |
+
# Agent not available or validation error
|
| 90 |
raise HTTPException(
|
| 91 |
status_code=status.HTTP_400_BAD_REQUEST,
|
| 92 |
detail=str(e)
|
|
|
|
| 112 |
result = await agent_service.invoke(
|
| 113 |
message=request.message,
|
| 114 |
model_name=request.model,
|
| 115 |
+
agent=request.agent,
|
| 116 |
temperature=request.temperature,
|
| 117 |
max_tokens=request.max_tokens,
|
| 118 |
conversation_history=request.conversation_history,
|
|
|
|
| 139 |
async for chunk in agent_service.stream(
|
| 140 |
message=request.message,
|
| 141 |
model_name=request.model,
|
| 142 |
+
agent=request.agent,
|
| 143 |
temperature=request.temperature,
|
| 144 |
max_tokens=request.max_tokens,
|
| 145 |
conversation_history=request.conversation_history,
|
docs/API_EXAMPLES.md
CHANGED
|
@@ -9,6 +9,11 @@
|
|
| 9 |
5. [WebSocket](#websocket)
|
| 10 |
6. [Exemples avancés](#exemples-avancés)
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
## Authentification
|
| 13 |
|
| 14 |
### Obtenir un token JWT
|
|
@@ -60,7 +65,7 @@ curl -X POST http://localhost:7860/completion \
|
|
| 60 |
-d '{
|
| 61 |
"message": "Explique-moi la théorie de la relativité en 2 phrases",
|
| 62 |
"model": "gpt-4o",
|
| 63 |
-
"
|
| 64 |
"stream": false,
|
| 65 |
"temperature": 0.7
|
| 66 |
}'
|
|
@@ -71,7 +76,7 @@ curl -X POST http://localhost:7860/completion \
|
|
| 71 |
{
|
| 72 |
"response": "La théorie de la relativité d'Einstein comprend deux parties: la relativité restreinte (1905) qui établit que la vitesse de la lumière est constante et que le temps et l'espace sont relatifs, et la relativité générale (1915) qui décrit la gravitation comme une courbure de l'espace-temps causée par la masse et l'énergie. Ces théories ont révolutionné notre compréhension de l'univers et sont confirmées par de nombreuses expériences.",
|
| 73 |
"model": "gpt-4o",
|
| 74 |
-
"
|
| 75 |
"usage": {
|
| 76 |
"prompt_tokens": 25,
|
| 77 |
"completion_tokens": 98,
|
|
@@ -99,15 +104,15 @@ curl -N -X POST http://localhost:7860/completion \
|
|
| 99 |
|
| 100 |
**Réponse (Server-Sent Events):**
|
| 101 |
```
|
| 102 |
-
data: {"content": "Il", "done": false, "metadata": {"model": "gpt-3.5-turbo", "
|
| 103 |
|
| 104 |
-
data: {"content": " était", "done": false, "metadata": {"model": "gpt-3.5-turbo", "
|
| 105 |
|
| 106 |
-
data: {"content": " une", "done": false, "metadata": {"model": "gpt-3.5-turbo", "
|
| 107 |
|
| 108 |
...
|
| 109 |
|
| 110 |
-
data: {"content": "", "done": true, "metadata": {"model": "gpt-3.5-turbo", "
|
| 111 |
```
|
| 112 |
|
| 113 |
#### Champs d'empreinte carbone, latence, pricing et équivalences
|
|
@@ -146,7 +151,7 @@ Les réponses incluent désormais des métriques d'impact carbone calculées ave
|
|
| 146 |
"done": true,
|
| 147 |
"metadata": {
|
| 148 |
"model": "mistral-large-latest",
|
| 149 |
-
"
|
| 150 |
"usage": {"input_tokens":123, "output_tokens":456, "total_tokens":579},
|
| 151 |
"usage_by_model": {
|
| 152 |
"mistral-large-latest": {"input_tokens":123, "output_tokens":456, "total_tokens":579}
|
|
@@ -324,19 +329,19 @@ curl -X GET http://localhost:7860/agents \
|
|
| 324 |
{
|
| 325 |
"agents": [
|
| 326 |
{
|
| 327 |
-
"type": "
|
| 328 |
-
"name": "
|
| 329 |
-
"description": "
|
| 330 |
"available": true
|
| 331 |
},
|
| 332 |
{
|
| 333 |
-
"type": "
|
| 334 |
-
"name": "
|
| 335 |
-
"description": "
|
| 336 |
-
"available":
|
| 337 |
}
|
| 338 |
],
|
| 339 |
-
"total":
|
| 340 |
}
|
| 341 |
```
|
| 342 |
|
|
|
|
| 9 |
5. [WebSocket](#websocket)
|
| 10 |
6. [Exemples avancés](#exemples-avancés)
|
| 11 |
|
| 12 |
+
## Point d'attention
|
| 13 |
+
|
| 14 |
+
- La completion HTTP utilise désormais `agent` (ex: `V1`, `V2`).
|
| 15 |
+
- Le pipeline voix conserve temporairement un champ interne nommé `agent_type` dans les événements (`services/voice/voice_pipeline.py`), pour compatibilité. Ce point est prévu pour un alignement ultérieur vers `agent`.
|
| 16 |
+
|
| 17 |
## Authentification
|
| 18 |
|
| 19 |
### Obtenir un token JWT
|
|
|
|
| 65 |
-d '{
|
| 66 |
"message": "Explique-moi la théorie de la relativité en 2 phrases",
|
| 67 |
"model": "gpt-4o",
|
| 68 |
+
"agent": "V2",
|
| 69 |
"stream": false,
|
| 70 |
"temperature": 0.7
|
| 71 |
}'
|
|
|
|
| 76 |
{
|
| 77 |
"response": "La théorie de la relativité d'Einstein comprend deux parties: la relativité restreinte (1905) qui établit que la vitesse de la lumière est constante et que le temps et l'espace sont relatifs, et la relativité générale (1915) qui décrit la gravitation comme une courbure de l'espace-temps causée par la masse et l'énergie. Ces théories ont révolutionné notre compréhension de l'univers et sont confirmées par de nombreuses expériences.",
|
| 78 |
"model": "gpt-4o",
|
| 79 |
+
"agent": "V2",
|
| 80 |
"usage": {
|
| 81 |
"prompt_tokens": 25,
|
| 82 |
"completion_tokens": 98,
|
|
|
|
| 104 |
|
| 105 |
**Réponse (Server-Sent Events):**
|
| 106 |
```
|
| 107 |
+
data: {"content": "Il", "done": false, "metadata": {"model": "gpt-3.5-turbo", "agent": "V2"}}
|
| 108 |
|
| 109 |
+
data: {"content": " était", "done": false, "metadata": {"model": "gpt-3.5-turbo", "agent": "V2"}}
|
| 110 |
|
| 111 |
+
data: {"content": " une", "done": false, "metadata": {"model": "gpt-3.5-turbo", "agent": "V2"}}
|
| 112 |
|
| 113 |
...
|
| 114 |
|
| 115 |
+
data: {"content": "", "done": true, "metadata": {"model": "gpt-3.5-turbo", "agent": "V2"}}
|
| 116 |
```
|
| 117 |
|
| 118 |
#### Champs d'empreinte carbone, latence, pricing et équivalences
|
|
|
|
| 151 |
"done": true,
|
| 152 |
"metadata": {
|
| 153 |
"model": "mistral-large-latest",
|
| 154 |
+
"agent": "V2",
|
| 155 |
"usage": {"input_tokens":123, "output_tokens":456, "total_tokens":579},
|
| 156 |
"usage_by_model": {
|
| 157 |
"mistral-large-latest": {"input_tokens":123, "output_tokens":456, "total_tokens":579}
|
|
|
|
| 329 |
{
|
| 330 |
"agents": [
|
| 331 |
{
|
| 332 |
+
"type": "V1",
|
| 333 |
+
"name": "V1",
|
| 334 |
+
"description": "Current production orchestrated workflow",
|
| 335 |
"available": true
|
| 336 |
},
|
| 337 |
{
|
| 338 |
+
"type": "V2",
|
| 339 |
+
"name": "V2",
|
| 340 |
+
"description": "Isolated V2 workflow (default)",
|
| 341 |
+
"available": true
|
| 342 |
}
|
| 343 |
],
|
| 344 |
+
"total": 2
|
| 345 |
}
|
| 346 |
```
|
| 347 |
|
domain/models.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
from pydantic import BaseModel, Field
|
| 3 |
from typing import Optional, List, Dict, Any, Literal
|
| 4 |
from datetime import datetime, timezone
|
| 5 |
-
from .enums import ModelName
|
| 6 |
|
| 7 |
|
| 8 |
# ============ Auth Models ============
|
|
@@ -27,7 +27,10 @@ class CompletionRequest(BaseModel):
|
|
| 27 |
"""Request for text completion."""
|
| 28 |
message: str = Field(..., description="User message to complete")
|
| 29 |
model: ModelName = Field(default=ModelName.MISTRAL_LARGE, description="LLM model to use")
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
| 31 |
stream: bool = Field(default=False, description="Enable streaming response")
|
| 32 |
temperature: float = Field(default=0.7, ge=0.0, le=2.0, description="Sampling temperature")
|
| 33 |
max_tokens: Optional[int] = Field(default=None, description="Maximum tokens to generate")
|
|
@@ -48,7 +51,7 @@ class CompletionResponse(BaseModel):
|
|
| 48 |
"""Response for text completion (non-streaming)."""
|
| 49 |
response: str
|
| 50 |
model: str
|
| 51 |
-
|
| 52 |
usage: Optional[Dict[str, Any]] = None
|
| 53 |
metadata: Optional[Dict[str, Any]] = None
|
| 54 |
conversation_id: Optional[str] = Field(
|
|
@@ -114,7 +117,7 @@ class ModelsListResponse(BaseModel):
|
|
| 114 |
|
| 115 |
class AgentInfo(BaseModel):
|
| 116 |
"""Information about an available agent."""
|
| 117 |
-
type:
|
| 118 |
name: str
|
| 119 |
description: str
|
| 120 |
available: bool = True
|
|
|
|
| 2 |
from pydantic import BaseModel, Field
|
| 3 |
from typing import Optional, List, Dict, Any, Literal
|
| 4 |
from datetime import datetime, timezone
|
| 5 |
+
from .enums import ModelName
|
| 6 |
|
| 7 |
|
| 8 |
# ============ Auth Models ============
|
|
|
|
| 27 |
"""Request for text completion."""
|
| 28 |
message: str = Field(..., description="User message to complete")
|
| 29 |
model: ModelName = Field(default=ModelName.MISTRAL_LARGE, description="LLM model to use")
|
| 30 |
+
agent: Optional[str] = Field(
|
| 31 |
+
default=None,
|
| 32 |
+
description="Agent identifier to use (ex: 'V1' or 'V2'). If omitted, defaults to 'V2'."
|
| 33 |
+
)
|
| 34 |
stream: bool = Field(default=False, description="Enable streaming response")
|
| 35 |
temperature: float = Field(default=0.7, ge=0.0, le=2.0, description="Sampling temperature")
|
| 36 |
max_tokens: Optional[int] = Field(default=None, description="Maximum tokens to generate")
|
|
|
|
| 51 |
"""Response for text completion (non-streaming)."""
|
| 52 |
response: str
|
| 53 |
model: str
|
| 54 |
+
agent: Optional[str] = None
|
| 55 |
usage: Optional[Dict[str, Any]] = None
|
| 56 |
metadata: Optional[Dict[str, Any]] = None
|
| 57 |
conversation_id: Optional[str] = Field(
|
|
|
|
| 117 |
|
| 118 |
class AgentInfo(BaseModel):
|
| 119 |
"""Information about an available agent."""
|
| 120 |
+
type: str
|
| 121 |
name: str
|
| 122 |
description: str
|
| 123 |
available: bool = True
|
graphs/README.md
DELETED
|
@@ -1,63 +0,0 @@
|
|
| 1 |
-
# LangGraph Graphs
|
| 2 |
-
|
| 3 |
-
Ce dossier contient les différents graphes LangGraph utilisés par l'API.
|
| 4 |
-
|
| 5 |
-
## Structure
|
| 6 |
-
|
| 7 |
-
- `base_graph.py`: Graphe conversationnel simple par défaut
|
| 8 |
-
- Vous pouvez ajouter d'autres graphes personnalisés ici
|
| 9 |
-
|
| 10 |
-
## Comment créer un nouveau graphe
|
| 11 |
-
|
| 12 |
-
1. Créez un nouveau fichier Python dans ce dossier (ex: `custom_graph.py`)
|
| 13 |
-
2. Définissez votre `State` avec TypedDict
|
| 14 |
-
3. Créez vos fonctions de nœuds
|
| 15 |
-
4. Construisez le graphe avec `StateGraph`
|
| 16 |
-
5. Compilez le graphe avec `.compile()`
|
| 17 |
-
6. Enregistrez votre graphe dans `services/agent_registry.py`
|
| 18 |
-
|
| 19 |
-
## Exemple de graphe personnalisé
|
| 20 |
-
|
| 21 |
-
```python
|
| 22 |
-
from typing import TypedDict, Annotated, Sequence
|
| 23 |
-
from langchain_core.messages import BaseMessage
|
| 24 |
-
from langgraph.graph import StateGraph, END
|
| 25 |
-
from langgraph.graph.message import add_messages
|
| 26 |
-
|
| 27 |
-
class CustomState(TypedDict):
|
| 28 |
-
messages: Annotated[Sequence[BaseMessage], add_messages]
|
| 29 |
-
custom_field: str
|
| 30 |
-
|
| 31 |
-
def create_custom_graph(llm):
|
| 32 |
-
def custom_node(state: CustomState):
|
| 33 |
-
# Votre logique personnalisée
|
| 34 |
-
messages = state["messages"]
|
| 35 |
-
response = llm.invoke(messages)
|
| 36 |
-
return {"messages": [response]}
|
| 37 |
-
|
| 38 |
-
workflow = StateGraph(CustomState)
|
| 39 |
-
workflow.add_node("custom", custom_node)
|
| 40 |
-
workflow.set_entry_point("custom")
|
| 41 |
-
workflow.add_edge("custom", END)
|
| 42 |
-
|
| 43 |
-
return workflow.compile()
|
| 44 |
-
```
|
| 45 |
-
|
| 46 |
-
## Graphes disponibles
|
| 47 |
-
|
| 48 |
-
### Simple Graph (`base_graph.py`)
|
| 49 |
-
- Graphe conversationnel basique
|
| 50 |
-
- Prend un message, l'envoie au LLM, retourne la réponse
|
| 51 |
-
- Pas de mémoire persistante
|
| 52 |
-
|
| 53 |
-
### Simple Graph with History (`base_graph.py`)
|
| 54 |
-
- Graphe conversationnel avec support de l'historique
|
| 55 |
-
- Utilise l'historique fourni dans la requête
|
| 56 |
-
- Pas de mémoire persistante (stateless)
|
| 57 |
-
|
| 58 |
-
## Notes
|
| 59 |
-
|
| 60 |
-
- Tous les graphes sont stateless par défaut
|
| 61 |
-
- L'historique de conversation doit être fourni par le client dans chaque requête
|
| 62 |
-
- Pour ajouter des outils (RAG, recherche web, etc.), créez un nouveau graphe personnalisé
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
graphs/base_graph.py
DELETED
|
@@ -1,193 +0,0 @@
|
|
| 1 |
-
"""Simple base LangGraph for conversational agent."""
|
| 2 |
-
from typing import TypedDict, Annotated, Sequence, List, Optional
|
| 3 |
-
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage
|
| 4 |
-
from langchain_core.language_models.chat_models import BaseChatModel
|
| 5 |
-
from langchain_core.documents import Document
|
| 6 |
-
from langgraph.graph import StateGraph, END
|
| 7 |
-
from langgraph.graph.message import add_messages
|
| 8 |
-
from .prompts import SYSTEM_PROMPT_TEMPLATE
|
| 9 |
-
|
| 10 |
-
# RAG imports (reuse setup from knowledge/ocr.ipynb)
|
| 11 |
-
import os
|
| 12 |
-
from functools import lru_cache
|
| 13 |
-
from supabase import create_client, Client
|
| 14 |
-
from langchain_openai import OpenAIEmbeddings
|
| 15 |
-
from langchain_community.vectorstores import SupabaseVectorStore
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
class AgentState(TypedDict, total=False):
|
| 19 |
-
"""State for the conversational agent with RAG."""
|
| 20 |
-
messages: Annotated[Sequence[BaseMessage], add_messages]
|
| 21 |
-
query: Optional[str]
|
| 22 |
-
formation_docs: List[Document]
|
| 23 |
-
prestation_docs: List[Document]
|
| 24 |
-
formation_context: str
|
| 25 |
-
prestation_context: str
|
| 26 |
-
project_docs: List[Document]
|
| 27 |
-
project_context: str
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def create_simple_graph(llm: BaseChatModel):
|
| 31 |
-
"""
|
| 32 |
-
Create a simple conversational graph with LangGraph.
|
| 33 |
-
|
| 34 |
-
This is a basic graph that takes a message, sends it to the LLM,
|
| 35 |
-
and returns the response. It can be easily replaced with more complex graphs.
|
| 36 |
-
|
| 37 |
-
Args:
|
| 38 |
-
llm: Language model to use for generation
|
| 39 |
-
|
| 40 |
-
Returns:
|
| 41 |
-
Compiled LangGraph
|
| 42 |
-
"""
|
| 43 |
-
|
| 44 |
-
def call_model(state: AgentState) -> AgentState:
|
| 45 |
-
"""Call the LLM with the current messages."""
|
| 46 |
-
print(f"Calling model with messages: {state['messages']}")
|
| 47 |
-
|
| 48 |
-
messages = state["messages"]
|
| 49 |
-
response = llm.invoke(messages)
|
| 50 |
-
return {"messages": messages + [AIMessage(content=response.content)] }
|
| 51 |
-
|
| 52 |
-
# Build the graph
|
| 53 |
-
workflow = StateGraph(AgentState)
|
| 54 |
-
|
| 55 |
-
# Add nodes
|
| 56 |
-
workflow.add_node("agent", call_model)
|
| 57 |
-
|
| 58 |
-
# Set entry point
|
| 59 |
-
workflow.set_entry_point("agent")
|
| 60 |
-
|
| 61 |
-
# Add edge to end
|
| 62 |
-
workflow.add_edge("agent", END)
|
| 63 |
-
|
| 64 |
-
# Compile and return
|
| 65 |
-
return workflow.compile()
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
def create_simple_graph_with_history(llm: BaseChatModel):
|
| 69 |
-
"""
|
| 70 |
-
Create a conversational graph with history + RAG retrieval from Supabase.
|
| 71 |
-
Entry -> retrieve (RAG) -> agent (generate) -> END
|
| 72 |
-
"""
|
| 73 |
-
|
| 74 |
-
@lru_cache(maxsize=2)
|
| 75 |
-
def _get_retriever(doc_type: str, k: int = int(os.getenv("RAG_TOP_K", "5"))):
|
| 76 |
-
"""Get retriever for specific document type (formation or prestation)."""
|
| 77 |
-
url = os.getenv("SUPABASE_URL")
|
| 78 |
-
key = (
|
| 79 |
-
os.getenv("SUPABASE_KEY")
|
| 80 |
-
or os.getenv("SUPABASE_SERVICE_ROLE_KEY")
|
| 81 |
-
or os.getenv("SUPABASE_ANON_KEY")
|
| 82 |
-
or os.getenv("NEXT_PUBLIC_SUPABASE_ANON_KEY")
|
| 83 |
-
)
|
| 84 |
-
if not url or not key:
|
| 85 |
-
raise ValueError("SUPABASE_URL and a SUPABASE_*KEY env var are required.")
|
| 86 |
-
client: Client = create_client(url, key)
|
| 87 |
-
vector_store = SupabaseVectorStore(
|
| 88 |
-
embedding=OpenAIEmbeddings(api_key=os.getenv("OPENAI_API_KEY")),
|
| 89 |
-
client=client,
|
| 90 |
-
table_name=os.getenv("SUPABASE_TABLE", "documents"),
|
| 91 |
-
query_name=os.getenv("SUPABASE_MATCH_FN", "match_documents"),
|
| 92 |
-
)
|
| 93 |
-
return vector_store.as_retriever(search_kwargs={"k": k, "filter": {"type": doc_type}})
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
def _format_docs(docs: List[Document], doc_type: str, max_chars_per_doc: int = 1200) -> str:
|
| 97 |
-
"""Format documents with type-specific formatting."""
|
| 98 |
-
blocks = []
|
| 99 |
-
for i, doc in enumerate(docs, 1):
|
| 100 |
-
text = (doc.page_content or "")[:max_chars_per_doc]
|
| 101 |
-
meta = doc.metadata or {}
|
| 102 |
-
src = meta.get("source", "N/A")
|
| 103 |
-
page = meta.get("page_number", "N/A")
|
| 104 |
-
kind = meta.get("type", "N/A")
|
| 105 |
-
contact = meta.get("contact", None)
|
| 106 |
-
header = f"[{i}] source={src} page={page} type={kind}"
|
| 107 |
-
if contact:
|
| 108 |
-
header += f" contact={contact}"
|
| 109 |
-
blocks.append(f"<document>\n{header}\n{text}</document>".strip())
|
| 110 |
-
return "\n\n---\n\n".join(blocks)
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
def retrieve(state: AgentState) -> AgentState:
|
| 114 |
-
"""Separate retriever node: builds query, fetches docs for both types, formats context."""
|
| 115 |
-
# Get query from state or last human message
|
| 116 |
-
q = state.get("query")
|
| 117 |
-
if not q:
|
| 118 |
-
q = ""
|
| 119 |
-
for msg in reversed(list(state.get("messages", []))):
|
| 120 |
-
if getattr(msg, "type", "") == "human":
|
| 121 |
-
q = (msg.content or "").strip()
|
| 122 |
-
break
|
| 123 |
-
|
| 124 |
-
# Get retrievers for both types
|
| 125 |
-
formation_retriever = _get_retriever("formation", k=8)
|
| 126 |
-
prestation_retriever = _get_retriever("prestation", k=8)
|
| 127 |
-
|
| 128 |
-
# Retrieve documents for both types
|
| 129 |
-
formation_docs = formation_retriever.invoke(q or "")
|
| 130 |
-
prestation_docs = prestation_retriever.invoke(q or "")
|
| 131 |
-
|
| 132 |
-
# Format contexts for both types
|
| 133 |
-
formation_context = _format_docs(formation_docs, "formation")
|
| 134 |
-
prestation_context = _format_docs(prestation_docs, "prestation")
|
| 135 |
-
|
| 136 |
-
return {
|
| 137 |
-
"formation_docs": formation_docs,
|
| 138 |
-
"prestation_docs": prestation_docs,
|
| 139 |
-
"formation_context": formation_context,
|
| 140 |
-
"prestation_context": prestation_context
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
def call_model_with_history(state: AgentState) -> AgentState:
|
| 145 |
-
"""Generation node: SYSTEM + RAG context + conversation."""
|
| 146 |
-
messages = list(state.get("messages", []))
|
| 147 |
-
sys_msgs: List[BaseMessage] = [SystemMessage(content=SYSTEM_PROMPT_TEMPLATE)]
|
| 148 |
-
|
| 149 |
-
# Get both contexts
|
| 150 |
-
formation_context = state.get("formation_context", "")
|
| 151 |
-
prestation_context = state.get("prestation_context", "")
|
| 152 |
-
|
| 153 |
-
# Add formation context if available
|
| 154 |
-
if formation_context:
|
| 155 |
-
sys_msgs.append(SystemMessage(content=(
|
| 156 |
-
"CONTEXTE FORMATIONS (extraits du catalogue formations; n'utilise rien d'autre):\n\n"
|
| 157 |
-
f"{formation_context}\n\n"
|
| 158 |
-
"Consignes formations: Utilise exclusivement ce contexte pour recommander les formations. "
|
| 159 |
-
"Cite la page et la source pour chaque recommandation. "
|
| 160 |
-
"Une formation = un document."
|
| 161 |
-
)))
|
| 162 |
-
|
| 163 |
-
# Add prestation context if available
|
| 164 |
-
if prestation_context:
|
| 165 |
-
sys_msgs.append(SystemMessage(content=(
|
| 166 |
-
"CONTEXTE PRESTATIONS (extraits du catalogue services; n'utilise rien d'autre):\n\n"
|
| 167 |
-
f"{prestation_context}\n\n"
|
| 168 |
-
"Consignes prestations: Utilise exclusivement ce contexte pour recommander les prestations. "
|
| 169 |
-
"Cite la page et la source pour chaque recommandation. "
|
| 170 |
-
"Un document peut contenir plusieurs prestations."
|
| 171 |
-
)))
|
| 172 |
-
|
| 173 |
-
response = llm.invoke(sys_msgs + messages)
|
| 174 |
-
return {"messages": messages + [AIMessage(content=response.content)]}
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
# Build the graph
|
| 178 |
-
workflow = StateGraph(AgentState)
|
| 179 |
-
|
| 180 |
-
# Add nodes
|
| 181 |
-
workflow.add_node("retrieve", retrieve)
|
| 182 |
-
workflow.add_node("agent", call_model_with_history)
|
| 183 |
-
|
| 184 |
-
# Set entry point
|
| 185 |
-
workflow.set_entry_point("retrieve")
|
| 186 |
-
|
| 187 |
-
# Add edges
|
| 188 |
-
workflow.add_edge("retrieve", "agent")
|
| 189 |
-
workflow.add_edge("agent", END)
|
| 190 |
-
|
| 191 |
-
# Compile and return
|
| 192 |
-
return workflow.compile()
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
graphs/workflows/conversation.py
DELETED
|
@@ -1,24 +0,0 @@
|
|
| 1 |
-
"""Conversation workflow: retrieve -> chat_agent -> END."""
|
| 2 |
-
from langgraph.graph import StateGraph, END
|
| 3 |
-
from langchain_core.language_models.chat_models import BaseChatModel
|
| 4 |
-
|
| 5 |
-
from graphs.state import AgentState
|
| 6 |
-
from graphs.nodes.retrieval import retrieve_both_types
|
| 7 |
-
from graphs.agents.chat_agent import chat_node
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
def create_conversation_graph(llm: BaseChatModel):
|
| 11 |
-
workflow = StateGraph(AgentState)
|
| 12 |
-
|
| 13 |
-
# Nodes
|
| 14 |
-
workflow.add_node("retrieve", retrieve_both_types)
|
| 15 |
-
workflow.add_node("agent", chat_node(llm))
|
| 16 |
-
|
| 17 |
-
# Entry and edges
|
| 18 |
-
workflow.set_entry_point("retrieve")
|
| 19 |
-
workflow.add_edge("retrieve", "agent")
|
| 20 |
-
workflow.add_edge("agent", END)
|
| 21 |
-
|
| 22 |
-
return workflow.compile()
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
graphs/workflows/{conversation_with_summary.py → orchestrated_v2.py}
RENAMED
|
@@ -1,20 +1,32 @@
|
|
| 1 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
from langgraph.graph import StateGraph, END
|
| 3 |
from langchain_core.language_models.chat_models import BaseChatModel
|
| 4 |
|
| 5 |
from graphs.state import AgentState
|
| 6 |
-
from graphs.
|
|
|
|
| 7 |
from graphs.agents.chat_agent import chat_node
|
| 8 |
from graphs.agents.summarizer_agent import summarizer_llm_node, summarizer_export_node
|
| 9 |
from tools.pdf import markdown_to_pdf
|
| 10 |
from tools.storage import upload_pdf_to_supabase
|
| 11 |
|
| 12 |
|
| 13 |
-
def
|
| 14 |
workflow = StateGraph(AgentState)
|
| 15 |
|
| 16 |
-
|
| 17 |
-
workflow.add_node("retrieve",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
workflow.add_node("agent", chat_node(llm))
|
| 19 |
workflow.add_node("summarizer_llm", summarizer_llm_node(llm))
|
| 20 |
workflow.add_node(
|
|
@@ -25,13 +37,33 @@ def create_conversation_with_summary_graph(llm: BaseChatModel):
|
|
| 25 |
),
|
| 26 |
)
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
workflow.add_edge("retrieve", "agent")
|
| 31 |
-
workflow.add_edge("agent",
|
|
|
|
| 32 |
workflow.add_edge("summarizer_llm", "summarizer_export")
|
| 33 |
workflow.add_edge("summarizer_export", END)
|
| 34 |
|
| 35 |
-
return workflow.compile()
|
| 36 |
-
|
| 37 |
|
|
|
|
| 1 |
+
"""Orchestrated V2 workflow.
|
| 2 |
+
|
| 3 |
+
V2 is intentionally isolated from V1 for safe incremental rollout.
|
| 4 |
+
Current behavior mirrors V1 and can evolve independently.
|
| 5 |
+
"""
|
| 6 |
from langgraph.graph import StateGraph, END
|
| 7 |
from langchain_core.language_models.chat_models import BaseChatModel
|
| 8 |
|
| 9 |
from graphs.state import AgentState
|
| 10 |
+
from graphs.agents.classifier_agent import classifier_node
|
| 11 |
+
from graphs.nodes.retrieval import retrieve_catalogue, retrieve_projects
|
| 12 |
from graphs.agents.chat_agent import chat_node
|
| 13 |
from graphs.agents.summarizer_agent import summarizer_llm_node, summarizer_export_node
|
| 14 |
from tools.pdf import markdown_to_pdf
|
| 15 |
from tools.storage import upload_pdf_to_supabase
|
| 16 |
|
| 17 |
|
| 18 |
+
def create_orchestrated_graph_v2(llm: BaseChatModel, checkpointer=None):
|
| 19 |
workflow = StateGraph(AgentState)
|
| 20 |
|
| 21 |
+
workflow.add_node("classify", classifier_node(llm))
|
| 22 |
+
workflow.add_node("retrieve", retrieve_catalogue)
|
| 23 |
+
|
| 24 |
+
def _router_passthrough(state: AgentState) -> AgentState:
|
| 25 |
+
q = state.get("query") or ""
|
| 26 |
+
return {"query": q}
|
| 27 |
+
|
| 28 |
+
workflow.add_node("retrieve_router", _router_passthrough)
|
| 29 |
+
workflow.add_node("retrieve_project", retrieve_projects)
|
| 30 |
workflow.add_node("agent", chat_node(llm))
|
| 31 |
workflow.add_node("summarizer_llm", summarizer_llm_node(llm))
|
| 32 |
workflow.add_node(
|
|
|
|
| 37 |
),
|
| 38 |
)
|
| 39 |
|
| 40 |
+
workflow.set_entry_point("classify")
|
| 41 |
+
|
| 42 |
+
workflow.add_conditional_edges(
|
| 43 |
+
"classify",
|
| 44 |
+
lambda s: getattr(s.get("classification"), "classification", "CLASSIC"),
|
| 45 |
+
{
|
| 46 |
+
"CLASSIC": "retrieve_router",
|
| 47 |
+
"SUMMARIZE": "summarizer_llm",
|
| 48 |
+
"UNKNOWN": "retrieve_router",
|
| 49 |
+
},
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
workflow.add_conditional_edges(
|
| 53 |
+
"retrieve_router",
|
| 54 |
+
lambda s: "PROJECT" if s.get("project_id") else "CLASSIC",
|
| 55 |
+
{
|
| 56 |
+
"PROJECT": "retrieve_project",
|
| 57 |
+
"CLASSIC": "retrieve",
|
| 58 |
+
},
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
workflow.add_edge("retrieve_project", "retrieve")
|
| 62 |
workflow.add_edge("retrieve", "agent")
|
| 63 |
+
workflow.add_edge("agent", END)
|
| 64 |
+
|
| 65 |
workflow.add_edge("summarizer_llm", "summarizer_export")
|
| 66 |
workflow.add_edge("summarizer_export", END)
|
| 67 |
|
| 68 |
+
return workflow.compile(checkpointer=checkpointer)
|
|
|
|
| 69 |
|
postman_collection.json
CHANGED
|
@@ -214,7 +214,7 @@
|
|
| 214 |
],
|
| 215 |
"body": {
|
| 216 |
"mode": "raw",
|
| 217 |
-
"raw": "{\n \"message\": \"Bonjour, comment vas-tu?\",\n \"model\": \"
|
| 218 |
},
|
| 219 |
"url": {
|
| 220 |
"raw": "{{base_url}}/completion",
|
|
@@ -251,7 +251,7 @@
|
|
| 251 |
],
|
| 252 |
"body": {
|
| 253 |
"mode": "raw",
|
| 254 |
-
"raw": "{\n \"message\": \"Explique-moi la théorie de la relativité en 2 phrases\",\n \"model\": \"mistral-large-latest\",\n \"
|
| 255 |
},
|
| 256 |
"url": {
|
| 257 |
"raw": "{{base_url}}/completion",
|
|
@@ -288,7 +288,7 @@
|
|
| 288 |
],
|
| 289 |
"body": {
|
| 290 |
"mode": "raw",
|
| 291 |
-
"raw": "{\n \"message\": \"Raconte-moi une courte histoire\",\n \"model\": \"
|
| 292 |
},
|
| 293 |
"url": {
|
| 294 |
"raw": "{{base_url}}/completion",
|
|
@@ -325,7 +325,7 @@
|
|
| 325 |
],
|
| 326 |
"body": {
|
| 327 |
"mode": "raw",
|
| 328 |
-
"raw": "{\n \"message\": \"Et en Python?\",\n \"model\": \"
|
| 329 |
},
|
| 330 |
"url": {
|
| 331 |
"raw": "{{base_url}}/completion",
|
|
@@ -362,7 +362,7 @@
|
|
| 362 |
],
|
| 363 |
"body": {
|
| 364 |
"mode": "raw",
|
| 365 |
-
"raw": "{\n \"message\": \"Écris un poème court sur l'IA\",\n \"model\": \"
|
| 366 |
},
|
| 367 |
"url": {
|
| 368 |
"raw": "{{base_url}}/completion",
|
|
|
|
| 214 |
],
|
| 215 |
"body": {
|
| 216 |
"mode": "raw",
|
| 217 |
+
"raw": "{\n \"message\": \"Bonjour, comment vas-tu?\",\n \"model\": \"mistral-large-latest\",\n \"agent\": \"V2\",\n \"stream\": false,\n \"temperature\": 0.7\n}"
|
| 218 |
},
|
| 219 |
"url": {
|
| 220 |
"raw": "{{base_url}}/completion",
|
|
|
|
| 251 |
],
|
| 252 |
"body": {
|
| 253 |
"mode": "raw",
|
| 254 |
+
"raw": "{\n \"message\": \"Explique-moi la théorie de la relativité en 2 phrases\",\n \"model\": \"mistral-large-latest\",\n \"agent\": \"V2\",\n \"stream\": false,\n \"temperature\": 0.7\n}"
|
| 255 |
},
|
| 256 |
"url": {
|
| 257 |
"raw": "{{base_url}}/completion",
|
|
|
|
| 288 |
],
|
| 289 |
"body": {
|
| 290 |
"mode": "raw",
|
| 291 |
+
"raw": "{\n \"message\": \"Raconte-moi une courte histoire\",\n \"model\": \"mistral-large-latest\",\n \"agent\": \"V2\",\n \"stream\": true,\n \"temperature\": 0.9\n}"
|
| 292 |
},
|
| 293 |
"url": {
|
| 294 |
"raw": "{{base_url}}/completion",
|
|
|
|
| 325 |
],
|
| 326 |
"body": {
|
| 327 |
"mode": "raw",
|
| 328 |
+
"raw": "{\n \"message\": \"Et en Python?\",\n \"model\": \"mistral-large-latest\",\n \"stream\": false,\n \"conversation_history\": [\n {\n \"role\": \"user\",\n \"content\": \"Comment faire une boucle en JavaScript?\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"En JavaScript, vous pouvez utiliser: for (let i = 0; i < 10; i++) { console.log(i); }\"\n }\n ]\n}"
|
| 329 |
},
|
| 330 |
"url": {
|
| 331 |
"raw": "{{base_url}}/completion",
|
|
|
|
| 362 |
],
|
| 363 |
"body": {
|
| 364 |
"mode": "raw",
|
| 365 |
+
"raw": "{\n \"message\": \"Écris un poème court sur l'IA\",\n \"model\": \"mistral-large-latest\",\n \"agent\": \"V2\",\n \"stream\": false,\n \"temperature\": 1.2,\n \"max_tokens\": 150\n}"
|
| 366 |
},
|
| 367 |
"url": {
|
| 368 |
"raw": "{{base_url}}/completion",
|
services/agent_registry.py
CHANGED
|
@@ -1,37 +1,32 @@
|
|
| 1 |
"""Registry for managing multiple LangGraph agents."""
|
| 2 |
-
from typing import Dict, Callable, Any
|
| 3 |
from langchain_core.language_models.chat_models import BaseChatModel
|
| 4 |
-
from domain.enums import AgentType
|
| 5 |
-
from graphs.base_graph import create_simple_graph, create_simple_graph_with_history
|
| 6 |
from graphs.workflows.orchestrated import create_orchestrated_graph
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
class AgentRegistry:
|
| 10 |
"""
|
| 11 |
Registry for managing multiple agent graph builders.
|
| 12 |
|
| 13 |
-
This allows for easy addition of new agent
|
| 14 |
-
the API layer. Each agent
|
| 15 |
"""
|
| 16 |
|
| 17 |
def __init__(self):
|
| 18 |
"""Initialize the agent registry with default agents."""
|
| 19 |
-
self.
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
# AgentType.TOOLS: create_tools_graph, # À implémenter plus tard
|
| 23 |
}
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
AgentType.RAG: "Agent with Retrieval Augmented Generation (not yet implemented)",
|
| 28 |
-
AgentType.TOOLS: "Agent with tools like web search, calculator (not yet implemented)",
|
| 29 |
-
AgentType.CUSTOM: "Custom agent graph (not yet implemented)"
|
| 30 |
}
|
| 31 |
|
| 32 |
def register_agent(
|
| 33 |
self,
|
| 34 |
-
|
| 35 |
builder: Callable[[BaseChatModel], Any],
|
| 36 |
description: str = ""
|
| 37 |
) -> None:
|
|
@@ -39,45 +34,34 @@ class AgentRegistry:
|
|
| 39 |
Register a new agent builder.
|
| 40 |
|
| 41 |
Args:
|
| 42 |
-
|
| 43 |
builder: Function that takes an LLM and returns a compiled graph
|
| 44 |
description: Description of the agent
|
| 45 |
"""
|
| 46 |
-
|
|
|
|
| 47 |
if description:
|
| 48 |
-
self._descriptions[
|
| 49 |
-
|
| 50 |
-
def
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
Returns:
|
| 58 |
-
Builder function
|
| 59 |
-
|
| 60 |
-
Raises:
|
| 61 |
-
ValueError: If agent type is not registered
|
| 62 |
"""
|
| 63 |
-
|
|
|
|
| 64 |
raise ValueError(
|
| 65 |
-
f"Agent
|
| 66 |
-
f"Available
|
| 67 |
)
|
| 68 |
-
return self.
|
| 69 |
-
|
| 70 |
-
def
|
| 71 |
-
"""
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
Args:
|
| 75 |
-
agent_type: Type of agent
|
| 76 |
-
|
| 77 |
-
Returns:
|
| 78 |
-
True if agent is available, False otherwise
|
| 79 |
-
"""
|
| 80 |
-
return agent_type in self._builders
|
| 81 |
|
| 82 |
def list_agents(self) -> list[dict]:
|
| 83 |
"""
|
|
@@ -86,18 +70,15 @@ class AgentRegistry:
|
|
| 86 |
Returns:
|
| 87 |
List of agent information dictionaries
|
| 88 |
"""
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
"
|
| 93 |
-
"
|
| 94 |
-
"
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
"available": self.is_available(agent_type)
|
| 99 |
-
})
|
| 100 |
-
return agents
|
| 101 |
|
| 102 |
|
| 103 |
# Singleton instance
|
|
|
|
| 1 |
"""Registry for managing multiple LangGraph agents."""
|
| 2 |
+
from typing import Dict, Callable, Any, Optional
|
| 3 |
from langchain_core.language_models.chat_models import BaseChatModel
|
|
|
|
|
|
|
| 4 |
from graphs.workflows.orchestrated import create_orchestrated_graph
|
| 5 |
+
from graphs.workflows.orchestrated_v2 import create_orchestrated_graph_v2
|
| 6 |
|
| 7 |
|
| 8 |
class AgentRegistry:
|
| 9 |
"""
|
| 10 |
Registry for managing multiple agent graph builders.
|
| 11 |
|
| 12 |
+
This allows for easy addition of new agent ids without modifying
|
| 13 |
+
the API layer. Each agent id maps to a graph builder function.
|
| 14 |
"""
|
| 15 |
|
| 16 |
def __init__(self):
|
| 17 |
"""Initialize the agent registry with default agents."""
|
| 18 |
+
self._agent_builders: Dict[str, Callable[[BaseChatModel], Any]] = {
|
| 19 |
+
"v1": create_orchestrated_graph, # V1 (unchanged)
|
| 20 |
+
"v2": create_orchestrated_graph_v2, # V2 (isolated)
|
|
|
|
| 21 |
}
|
| 22 |
+
self._descriptions: Dict[str, str] = {
|
| 23 |
+
"v1": "Current production orchestrated workflow",
|
| 24 |
+
"v2": "Isolated V2 workflow (default)",
|
|
|
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
def register_agent(
|
| 28 |
self,
|
| 29 |
+
agent: str,
|
| 30 |
builder: Callable[[BaseChatModel], Any],
|
| 31 |
description: str = ""
|
| 32 |
) -> None:
|
|
|
|
| 34 |
Register a new agent builder.
|
| 35 |
|
| 36 |
Args:
|
| 37 |
+
agent: Agent identifier (string)
|
| 38 |
builder: Function that takes an LLM and returns a compiled graph
|
| 39 |
description: Description of the agent
|
| 40 |
"""
|
| 41 |
+
agent_key = agent.strip().lower()
|
| 42 |
+
self._agent_builders[agent_key] = builder
|
| 43 |
if description:
|
| 44 |
+
self._descriptions[agent_key] = description
|
| 45 |
+
|
| 46 |
+
def get_builder_for_request(
|
| 47 |
+
self,
|
| 48 |
+
agent: Optional[str],
|
| 49 |
+
) -> Callable[[BaseChatModel], Any]:
|
| 50 |
+
"""Resolve graph builder from request.
|
| 51 |
+
|
| 52 |
+
If agent is missing, defaults to V2.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
"""
|
| 54 |
+
agent_key = (agent or "V2").strip().lower()
|
| 55 |
+
if agent_key not in self._agent_builders:
|
| 56 |
raise ValueError(
|
| 57 |
+
f"Agent '{agent}' not implemented. "
|
| 58 |
+
f"Available agents: {list(self._agent_builders.keys())}"
|
| 59 |
)
|
| 60 |
+
return self._agent_builders[agent_key]
|
| 61 |
+
|
| 62 |
+
def resolve_agent_id(self, agent: Optional[str]) -> str:
|
| 63 |
+
"""Return canonical agent id used in API metadata."""
|
| 64 |
+
return (agent or "V2").strip().upper()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
def list_agents(self) -> list[dict]:
|
| 67 |
"""
|
|
|
|
| 70 |
Returns:
|
| 71 |
List of agent information dictionaries
|
| 72 |
"""
|
| 73 |
+
return [
|
| 74 |
+
{
|
| 75 |
+
"type": key.upper(),
|
| 76 |
+
"name": key.upper(),
|
| 77 |
+
"description": self._descriptions.get(key, "No description available"),
|
| 78 |
+
"available": True,
|
| 79 |
+
}
|
| 80 |
+
for key in sorted(self._agent_builders.keys())
|
| 81 |
+
]
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
# Singleton instance
|
services/agent_service.py
CHANGED
|
@@ -4,7 +4,7 @@ import time
|
|
| 4 |
from langchain_core.messages import AIMessageChunk, HumanMessage, AIMessage, BaseMessage, SystemMessage
|
| 5 |
from langchain_core.language_models.chat_models import BaseChatModel
|
| 6 |
from langgraph.checkpoint.memory import MemorySaver
|
| 7 |
-
from domain.enums import ModelName
|
| 8 |
from .llm_service import llm_service
|
| 9 |
from .agent_registry import agent_registry
|
| 10 |
from .usage_utils import normalize_usage
|
|
@@ -34,7 +34,7 @@ class AgentService:
|
|
| 34 |
self,
|
| 35 |
message: str,
|
| 36 |
model_name: ModelName,
|
| 37 |
-
|
| 38 |
temperature: float = 0.7,
|
| 39 |
max_tokens: Optional[int] = None,
|
| 40 |
conversation_history: Optional[List[Dict[str, str]]] = None,
|
|
@@ -47,7 +47,7 @@ class AgentService:
|
|
| 47 |
Args:
|
| 48 |
message: User message
|
| 49 |
model_name: LLM model to use
|
| 50 |
-
|
| 51 |
temperature: Sampling temperature
|
| 52 |
max_tokens: Max tokens to generate
|
| 53 |
conversation_history: Optional conversation history (ignored if conversation_id is set)
|
|
@@ -65,7 +65,8 @@ class AgentService:
|
|
| 65 |
max_tokens=max_tokens
|
| 66 |
)
|
| 67 |
|
| 68 |
-
|
|
|
|
| 69 |
use_memory = bool(conversation_id)
|
| 70 |
graph = builder(llm, checkpointer=_text_checkpointer) if use_memory else builder(llm)
|
| 71 |
|
|
@@ -114,7 +115,7 @@ class AgentService:
|
|
| 114 |
return {
|
| 115 |
"response": response_content,
|
| 116 |
"model": model_name.value,
|
| 117 |
-
"
|
| 118 |
"usage": usage,
|
| 119 |
"metadata": base_metadata,
|
| 120 |
}
|
|
@@ -123,7 +124,7 @@ class AgentService:
|
|
| 123 |
self,
|
| 124 |
message: str,
|
| 125 |
model_name: ModelName,
|
| 126 |
-
|
| 127 |
temperature: float = 0.7,
|
| 128 |
max_tokens: Optional[int] = None,
|
| 129 |
conversation_history: Optional[List[Dict[str, str]]] = None,
|
|
@@ -136,7 +137,7 @@ class AgentService:
|
|
| 136 |
Args:
|
| 137 |
message: User message
|
| 138 |
model_name: LLM model to use
|
| 139 |
-
|
| 140 |
temperature: Sampling temperature
|
| 141 |
max_tokens: Max tokens to generate
|
| 142 |
conversation_history: Optional conversation history (ignored if conversation_id is set)
|
|
@@ -154,7 +155,8 @@ class AgentService:
|
|
| 154 |
max_tokens=max_tokens
|
| 155 |
)
|
| 156 |
|
| 157 |
-
|
|
|
|
| 158 |
use_memory = bool(conversation_id)
|
| 159 |
graph = builder(llm, checkpointer=_text_checkpointer) if use_memory else builder(llm)
|
| 160 |
|
|
@@ -255,7 +257,7 @@ class AgentService:
|
|
| 255 |
"done": False,
|
| 256 |
"metadata": {
|
| 257 |
"model": model_name.value,
|
| 258 |
-
"
|
| 259 |
"usage": usage_totals
|
| 260 |
},
|
| 261 |
"documents": documents
|
|
@@ -278,7 +280,7 @@ class AgentService:
|
|
| 278 |
"done": True,
|
| 279 |
"metadata": {
|
| 280 |
"model": model_name.value,
|
| 281 |
-
"
|
| 282 |
"usage": usage_totals,
|
| 283 |
"usage_by_model": usage_by_model,
|
| 284 |
"latency_s": latency_s,
|
|
|
|
| 4 |
from langchain_core.messages import AIMessageChunk, HumanMessage, AIMessage, BaseMessage, SystemMessage
|
| 5 |
from langchain_core.language_models.chat_models import BaseChatModel
|
| 6 |
from langgraph.checkpoint.memory import MemorySaver
|
| 7 |
+
from domain.enums import ModelName
|
| 8 |
from .llm_service import llm_service
|
| 9 |
from .agent_registry import agent_registry
|
| 10 |
from .usage_utils import normalize_usage
|
|
|
|
| 34 |
self,
|
| 35 |
message: str,
|
| 36 |
model_name: ModelName,
|
| 37 |
+
agent: Optional[str] = None,
|
| 38 |
temperature: float = 0.7,
|
| 39 |
max_tokens: Optional[int] = None,
|
| 40 |
conversation_history: Optional[List[Dict[str, str]]] = None,
|
|
|
|
| 47 |
Args:
|
| 48 |
message: User message
|
| 49 |
model_name: LLM model to use
|
| 50 |
+
agent: Agent identifier. Defaults to "V2" when omitted.
|
| 51 |
temperature: Sampling temperature
|
| 52 |
max_tokens: Max tokens to generate
|
| 53 |
conversation_history: Optional conversation history (ignored if conversation_id is set)
|
|
|
|
| 65 |
max_tokens=max_tokens
|
| 66 |
)
|
| 67 |
|
| 68 |
+
resolved_agent = agent_registry.resolve_agent_id(agent)
|
| 69 |
+
builder = agent_registry.get_builder_for_request(agent=resolved_agent)
|
| 70 |
use_memory = bool(conversation_id)
|
| 71 |
graph = builder(llm, checkpointer=_text_checkpointer) if use_memory else builder(llm)
|
| 72 |
|
|
|
|
| 115 |
return {
|
| 116 |
"response": response_content,
|
| 117 |
"model": model_name.value,
|
| 118 |
+
"agent": resolved_agent,
|
| 119 |
"usage": usage,
|
| 120 |
"metadata": base_metadata,
|
| 121 |
}
|
|
|
|
| 124 |
self,
|
| 125 |
message: str,
|
| 126 |
model_name: ModelName,
|
| 127 |
+
agent: Optional[str] = None,
|
| 128 |
temperature: float = 0.7,
|
| 129 |
max_tokens: Optional[int] = None,
|
| 130 |
conversation_history: Optional[List[Dict[str, str]]] = None,
|
|
|
|
| 137 |
Args:
|
| 138 |
message: User message
|
| 139 |
model_name: LLM model to use
|
| 140 |
+
agent: Agent identifier. Defaults to "V2" when omitted.
|
| 141 |
temperature: Sampling temperature
|
| 142 |
max_tokens: Max tokens to generate
|
| 143 |
conversation_history: Optional conversation history (ignored if conversation_id is set)
|
|
|
|
| 155 |
max_tokens=max_tokens
|
| 156 |
)
|
| 157 |
|
| 158 |
+
resolved_agent = agent_registry.resolve_agent_id(agent)
|
| 159 |
+
builder = agent_registry.get_builder_for_request(agent=resolved_agent)
|
| 160 |
use_memory = bool(conversation_id)
|
| 161 |
graph = builder(llm, checkpointer=_text_checkpointer) if use_memory else builder(llm)
|
| 162 |
|
|
|
|
| 257 |
"done": False,
|
| 258 |
"metadata": {
|
| 259 |
"model": model_name.value,
|
| 260 |
+
"agent": resolved_agent,
|
| 261 |
"usage": usage_totals
|
| 262 |
},
|
| 263 |
"documents": documents
|
|
|
|
| 280 |
"done": True,
|
| 281 |
"metadata": {
|
| 282 |
"model": model_name.value,
|
| 283 |
+
"agent": resolved_agent,
|
| 284 |
"usage": usage_totals,
|
| 285 |
"usage_by_model": usage_by_model,
|
| 286 |
"latency_s": latency_s,
|
services/voice/voice_agent_service.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import Optional
|
|
| 5 |
from langchain_core.messages import HumanMessage
|
| 6 |
from langgraph.checkpoint.memory import MemorySaver
|
| 7 |
|
| 8 |
-
from domain.enums import ModelName
|
| 9 |
from services.llm_service import llm_service
|
| 10 |
from services.agent_registry import agent_registry
|
| 11 |
|
|
@@ -29,7 +29,7 @@ class VoiceAgentService:
|
|
| 29 |
self.checkpointer = MemorySaver()
|
| 30 |
|
| 31 |
llm = llm_service.get_llm(model_name=model_name, streaming=False)
|
| 32 |
-
builder = agent_registry.
|
| 33 |
self.graph = builder(llm, checkpointer=self.checkpointer)
|
| 34 |
|
| 35 |
async def process_message(
|
|
|
|
| 5 |
from langchain_core.messages import HumanMessage
|
| 6 |
from langgraph.checkpoint.memory import MemorySaver
|
| 7 |
|
| 8 |
+
from domain.enums import ModelName
|
| 9 |
from services.llm_service import llm_service
|
| 10 |
from services.agent_registry import agent_registry
|
| 11 |
|
|
|
|
| 29 |
self.checkpointer = MemorySaver()
|
| 30 |
|
| 31 |
llm = llm_service.get_llm(model_name=model_name, streaming=False)
|
| 32 |
+
builder = agent_registry.get_builder_for_request("V1")
|
| 33 |
self.graph = builder(llm, checkpointer=self.checkpointer)
|
| 34 |
|
| 35 |
async def process_message(
|
services/voice/voice_pipeline.py
CHANGED
|
@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
|
|
| 6 |
|
| 7 |
from langchain_core.runnables import RunnableGenerator
|
| 8 |
|
| 9 |
-
from domain.enums import ModelName
|
| 10 |
from services.agent_service import agent_service
|
| 11 |
from .stt_service import STTService, stt_service
|
| 12 |
from .tts_service import TTSService, tts_service, TTSVoice
|
|
@@ -28,7 +28,7 @@ logger = logging.getLogger(__name__)
|
|
| 28 |
class VoiceSessionConfig:
|
| 29 |
"""Configuration for a voice session."""
|
| 30 |
model: ModelName = ModelName.MISTRAL_LARGE
|
| 31 |
-
|
| 32 |
voice: TTSVoice = "alloy"
|
| 33 |
language: Optional[str] = None
|
| 34 |
temperature: float = 0.7
|
|
@@ -192,7 +192,7 @@ class VoicePipeline:
|
|
| 192 |
async for chunk in agent_service.stream(
|
| 193 |
message=text,
|
| 194 |
model_name=config.model,
|
| 195 |
-
|
| 196 |
temperature=config.temperature,
|
| 197 |
max_tokens=config.max_tokens,
|
| 198 |
conversation_history=config.conversation_history,
|
|
@@ -209,7 +209,7 @@ class VoicePipeline:
|
|
| 209 |
yield AgentOutputEvent.create(
|
| 210 |
response=full_response,
|
| 211 |
model=metadata.get("model"),
|
| 212 |
-
agent_type=metadata.get("
|
| 213 |
usage=metadata.get("usage")
|
| 214 |
)
|
| 215 |
|
|
|
|
| 6 |
|
| 7 |
from langchain_core.runnables import RunnableGenerator
|
| 8 |
|
| 9 |
+
from domain.enums import ModelName
|
| 10 |
from services.agent_service import agent_service
|
| 11 |
from .stt_service import STTService, stt_service
|
| 12 |
from .tts_service import TTSService, tts_service, TTSVoice
|
|
|
|
| 28 |
class VoiceSessionConfig:
|
| 29 |
"""Configuration for a voice session."""
|
| 30 |
model: ModelName = ModelName.MISTRAL_LARGE
|
| 31 |
+
agent: Optional[str] = None # Defaults to V2 when omitted
|
| 32 |
voice: TTSVoice = "alloy"
|
| 33 |
language: Optional[str] = None
|
| 34 |
temperature: float = 0.7
|
|
|
|
| 192 |
async for chunk in agent_service.stream(
|
| 193 |
message=text,
|
| 194 |
model_name=config.model,
|
| 195 |
+
agent=config.agent,
|
| 196 |
temperature=config.temperature,
|
| 197 |
max_tokens=config.max_tokens,
|
| 198 |
conversation_history=config.conversation_history,
|
|
|
|
| 209 |
yield AgentOutputEvent.create(
|
| 210 |
response=full_response,
|
| 211 |
model=metadata.get("model"),
|
| 212 |
+
agent_type=metadata.get("agent"),
|
| 213 |
usage=metadata.get("usage")
|
| 214 |
)
|
| 215 |
|