naas-abi-core 1.4.1__py3-none-any.whl
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.
- assets/favicon.ico +0 -0
- assets/logo.png +0 -0
- naas_abi_core/__init__.py +1 -0
- naas_abi_core/apps/api/api.py +245 -0
- naas_abi_core/apps/api/api_test.py +281 -0
- naas_abi_core/apps/api/openapi_doc.py +144 -0
- naas_abi_core/apps/mcp/Dockerfile.mcp +35 -0
- naas_abi_core/apps/mcp/mcp_server.py +243 -0
- naas_abi_core/apps/mcp/mcp_server_test.py +163 -0
- naas_abi_core/apps/terminal_agent/main.py +555 -0
- naas_abi_core/apps/terminal_agent/terminal_style.py +175 -0
- naas_abi_core/engine/Engine.py +87 -0
- naas_abi_core/engine/EngineProxy.py +109 -0
- naas_abi_core/engine/Engine_test.py +6 -0
- naas_abi_core/engine/IEngine.py +91 -0
- naas_abi_core/engine/conftest.py +45 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration.py +216 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_Deploy.py +7 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_GenericLoader.py +49 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService.py +159 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService_test.py +26 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService.py +138 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService_test.py +74 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService.py +224 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService_test.py +109 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService.py +76 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService_test.py +33 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_test.py +9 -0
- naas_abi_core/engine/engine_configuration/utils/PydanticModelValidator.py +15 -0
- naas_abi_core/engine/engine_loaders/EngineModuleLoader.py +302 -0
- naas_abi_core/engine/engine_loaders/EngineOntologyLoader.py +16 -0
- naas_abi_core/engine/engine_loaders/EngineServiceLoader.py +47 -0
- naas_abi_core/integration/__init__.py +7 -0
- naas_abi_core/integration/integration.py +28 -0
- naas_abi_core/models/Model.py +198 -0
- naas_abi_core/models/OpenRouter.py +18 -0
- naas_abi_core/models/OpenRouter_test.py +36 -0
- naas_abi_core/module/Module.py +252 -0
- naas_abi_core/module/ModuleAgentLoader.py +50 -0
- naas_abi_core/module/ModuleUtils.py +20 -0
- naas_abi_core/modules/templatablesparqlquery/README.md +196 -0
- naas_abi_core/modules/templatablesparqlquery/__init__.py +39 -0
- naas_abi_core/modules/templatablesparqlquery/ontologies/TemplatableSparqlQueryOntology.ttl +116 -0
- naas_abi_core/modules/templatablesparqlquery/workflows/GenericWorkflow.py +48 -0
- naas_abi_core/modules/templatablesparqlquery/workflows/TemplatableSparqlQueryLoader.py +192 -0
- naas_abi_core/pipeline/__init__.py +6 -0
- naas_abi_core/pipeline/pipeline.py +70 -0
- naas_abi_core/services/__init__.py +0 -0
- naas_abi_core/services/agent/Agent.py +1619 -0
- naas_abi_core/services/agent/AgentMemory_test.py +28 -0
- naas_abi_core/services/agent/Agent_test.py +214 -0
- naas_abi_core/services/agent/IntentAgent.py +1179 -0
- naas_abi_core/services/agent/IntentAgent_test.py +139 -0
- naas_abi_core/services/agent/beta/Embeddings.py +181 -0
- naas_abi_core/services/agent/beta/IntentMapper.py +120 -0
- naas_abi_core/services/agent/beta/LocalModel.py +88 -0
- naas_abi_core/services/agent/beta/VectorStore.py +89 -0
- naas_abi_core/services/agent/test_agent_memory.py +278 -0
- naas_abi_core/services/agent/test_postgres_integration.py +145 -0
- naas_abi_core/services/cache/CacheFactory.py +31 -0
- naas_abi_core/services/cache/CachePort.py +63 -0
- naas_abi_core/services/cache/CacheService.py +246 -0
- naas_abi_core/services/cache/CacheService_test.py +85 -0
- naas_abi_core/services/cache/adapters/secondary/CacheFSAdapter.py +39 -0
- naas_abi_core/services/object_storage/ObjectStorageFactory.py +57 -0
- naas_abi_core/services/object_storage/ObjectStoragePort.py +47 -0
- naas_abi_core/services/object_storage/ObjectStorageService.py +41 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterFS.py +52 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterNaas.py +131 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterS3.py +171 -0
- naas_abi_core/services/ontology/OntologyPorts.py +36 -0
- naas_abi_core/services/ontology/OntologyService.py +17 -0
- naas_abi_core/services/ontology/adaptors/secondary/OntologyService_SecondaryAdaptor_NERPort.py +37 -0
- naas_abi_core/services/secret/Secret.py +138 -0
- naas_abi_core/services/secret/SecretPorts.py +45 -0
- naas_abi_core/services/secret/Secret_test.py +65 -0
- naas_abi_core/services/secret/adaptors/secondary/Base64Secret.py +57 -0
- naas_abi_core/services/secret/adaptors/secondary/Base64Secret_test.py +39 -0
- naas_abi_core/services/secret/adaptors/secondary/NaasSecret.py +88 -0
- naas_abi_core/services/secret/adaptors/secondary/NaasSecret_test.py +25 -0
- naas_abi_core/services/secret/adaptors/secondary/dotenv_secret_secondaryadaptor.py +29 -0
- naas_abi_core/services/triple_store/TripleStoreFactory.py +116 -0
- naas_abi_core/services/triple_store/TripleStorePorts.py +223 -0
- naas_abi_core/services/triple_store/TripleStoreService.py +419 -0
- naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune.py +1300 -0
- naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune_test.py +284 -0
- naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph.py +597 -0
- naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph_test.py +1474 -0
- naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__Filesystem.py +223 -0
- naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__ObjectStorage.py +234 -0
- naas_abi_core/services/triple_store/adaptors/secondary/base/TripleStoreService__SecondaryAdaptor__FileBase.py +18 -0
- naas_abi_core/services/vector_store/IVectorStorePort.py +101 -0
- naas_abi_core/services/vector_store/IVectorStorePort_test.py +189 -0
- naas_abi_core/services/vector_store/VectorStoreFactory.py +47 -0
- naas_abi_core/services/vector_store/VectorStoreService.py +171 -0
- naas_abi_core/services/vector_store/VectorStoreService_test.py +185 -0
- naas_abi_core/services/vector_store/__init__.py +13 -0
- naas_abi_core/services/vector_store/adapters/QdrantAdapter.py +251 -0
- naas_abi_core/services/vector_store/adapters/QdrantAdapter_test.py +57 -0
- naas_abi_core/tests/test_services_imports.py +69 -0
- naas_abi_core/utils/Expose.py +55 -0
- naas_abi_core/utils/Graph.py +182 -0
- naas_abi_core/utils/JSON.py +49 -0
- naas_abi_core/utils/LazyLoader.py +44 -0
- naas_abi_core/utils/Logger.py +12 -0
- naas_abi_core/utils/OntologyReasoner.py +141 -0
- naas_abi_core/utils/OntologyYaml.py +681 -0
- naas_abi_core/utils/SPARQL.py +256 -0
- naas_abi_core/utils/Storage.py +33 -0
- naas_abi_core/utils/StorageUtils.py +398 -0
- naas_abi_core/utils/String.py +52 -0
- naas_abi_core/utils/Workers.py +114 -0
- naas_abi_core/utils/__init__.py +0 -0
- naas_abi_core/utils/onto2py/README.md +0 -0
- naas_abi_core/utils/onto2py/__init__.py +10 -0
- naas_abi_core/utils/onto2py/__main__.py +29 -0
- naas_abi_core/utils/onto2py/onto2py.py +611 -0
- naas_abi_core/utils/onto2py/tests/ttl2py_test.py +271 -0
- naas_abi_core/workflow/__init__.py +5 -0
- naas_abi_core/workflow/workflow.py +48 -0
- naas_abi_core-1.4.1.dist-info/METADATA +630 -0
- naas_abi_core-1.4.1.dist-info/RECORD +124 -0
- naas_abi_core-1.4.1.dist-info/WHEEL +4 -0
- naas_abi_core-1.4.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,1179 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from queue import Queue
|
|
3
|
+
from typing import Any, Callable, Dict, Optional, Union
|
|
4
|
+
|
|
5
|
+
import pydash as pd
|
|
6
|
+
import spacy
|
|
7
|
+
from langchain_core.language_models import BaseChatModel
|
|
8
|
+
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
|
|
9
|
+
from langchain_core.tools import BaseTool, Tool, tool
|
|
10
|
+
from langgraph.checkpoint.base import BaseCheckpointSaver
|
|
11
|
+
from langgraph.graph import END, START, StateGraph
|
|
12
|
+
from langgraph.graph.message import MessagesState
|
|
13
|
+
from langgraph.types import Command
|
|
14
|
+
from naas_abi_core import logger
|
|
15
|
+
from naas_abi_core.models.Model import ChatModel
|
|
16
|
+
from spacy.cli import download as spacy_download
|
|
17
|
+
|
|
18
|
+
from .Agent import Agent, AgentConfiguration, AgentSharedState, create_checkpointer
|
|
19
|
+
from .beta.IntentMapper import Intent, IntentMapper, IntentScope, IntentType
|
|
20
|
+
|
|
21
|
+
_nlp = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_nlp():
|
|
25
|
+
global _nlp
|
|
26
|
+
if _nlp is None:
|
|
27
|
+
try:
|
|
28
|
+
_nlp = spacy.load("en_core_web_sm")
|
|
29
|
+
except OSError:
|
|
30
|
+
logger.warning(
|
|
31
|
+
"Downloading spacy model en_core_web_sm as it is not installed. This is a one time operation and can take a few seconds to complete based on your internet connection speed."
|
|
32
|
+
)
|
|
33
|
+
spacy_download("en_core_web_sm")
|
|
34
|
+
_nlp = spacy.load("en_core_web_sm")
|
|
35
|
+
return _nlp
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
MULTIPLES_INTENTS_MESSAGE = "I found multiple intents that could handle your request"
|
|
39
|
+
DEFAULT_INTENTS: list = [
|
|
40
|
+
Intent(
|
|
41
|
+
intent_value="what's your name?",
|
|
42
|
+
intent_type=IntentType.AGENT,
|
|
43
|
+
intent_target="call_model",
|
|
44
|
+
intent_scope=IntentScope.DIRECT,
|
|
45
|
+
),
|
|
46
|
+
Intent(
|
|
47
|
+
intent_value="what do you do?",
|
|
48
|
+
intent_type=IntentType.AGENT,
|
|
49
|
+
intent_target="call_model",
|
|
50
|
+
intent_scope=IntentScope.DIRECT,
|
|
51
|
+
),
|
|
52
|
+
Intent(
|
|
53
|
+
intent_value="comment tu t'appelles?",
|
|
54
|
+
intent_type=IntentType.AGENT,
|
|
55
|
+
intent_target="call_model",
|
|
56
|
+
intent_scope=IntentScope.DIRECT,
|
|
57
|
+
),
|
|
58
|
+
Intent(
|
|
59
|
+
intent_value="que fais-tu?",
|
|
60
|
+
intent_type=IntentType.AGENT,
|
|
61
|
+
intent_target="call_model",
|
|
62
|
+
intent_scope=IntentScope.DIRECT,
|
|
63
|
+
),
|
|
64
|
+
Intent(
|
|
65
|
+
intent_value="Hello",
|
|
66
|
+
intent_type=IntentType.RAW,
|
|
67
|
+
intent_target="Hello, what can I do for you?",
|
|
68
|
+
intent_scope=IntentScope.DIRECT,
|
|
69
|
+
),
|
|
70
|
+
Intent(
|
|
71
|
+
intent_value="Hi",
|
|
72
|
+
intent_type=IntentType.RAW,
|
|
73
|
+
intent_target="Hello, what can I do for you?",
|
|
74
|
+
intent_scope=IntentScope.DIRECT,
|
|
75
|
+
),
|
|
76
|
+
Intent(
|
|
77
|
+
intent_value="Hey",
|
|
78
|
+
intent_type=IntentType.RAW,
|
|
79
|
+
intent_target="Hello, what can I do for you?",
|
|
80
|
+
intent_scope=IntentScope.DIRECT,
|
|
81
|
+
),
|
|
82
|
+
Intent(
|
|
83
|
+
intent_value="Salut",
|
|
84
|
+
intent_type=IntentType.RAW,
|
|
85
|
+
intent_target="Bonjour, que puis-je faire pour vous?",
|
|
86
|
+
intent_scope=IntentScope.DIRECT,
|
|
87
|
+
),
|
|
88
|
+
Intent(
|
|
89
|
+
intent_value="Bonjour",
|
|
90
|
+
intent_type=IntentType.RAW,
|
|
91
|
+
intent_target="Bonjour, que puis-je faire pour vous?",
|
|
92
|
+
intent_scope=IntentScope.DIRECT,
|
|
93
|
+
),
|
|
94
|
+
Intent(
|
|
95
|
+
intent_value="Coucou",
|
|
96
|
+
intent_type=IntentType.RAW,
|
|
97
|
+
intent_target="Bonjour, que puis-je faire pour vous?",
|
|
98
|
+
intent_scope=IntentScope.DIRECT,
|
|
99
|
+
),
|
|
100
|
+
Intent(
|
|
101
|
+
intent_value="Hi there",
|
|
102
|
+
intent_type=IntentType.RAW,
|
|
103
|
+
intent_target="Hello, what can I do for you?",
|
|
104
|
+
intent_scope=IntentScope.DIRECT,
|
|
105
|
+
),
|
|
106
|
+
Intent(
|
|
107
|
+
intent_value="Hello there",
|
|
108
|
+
intent_type=IntentType.RAW,
|
|
109
|
+
intent_target="Hello, what can I do for you?",
|
|
110
|
+
intent_scope=IntentScope.DIRECT,
|
|
111
|
+
),
|
|
112
|
+
Intent(
|
|
113
|
+
intent_value="Hello, how are you?",
|
|
114
|
+
intent_type=IntentType.RAW,
|
|
115
|
+
intent_target="Hello, I am doing well thank you, how can I help you today?",
|
|
116
|
+
intent_scope=IntentScope.DIRECT,
|
|
117
|
+
),
|
|
118
|
+
Intent(
|
|
119
|
+
intent_value="Hi, how are you?",
|
|
120
|
+
intent_type=IntentType.RAW,
|
|
121
|
+
intent_target="Hello, I am doing well thank you, how can I help you today?",
|
|
122
|
+
intent_scope=IntentScope.DIRECT,
|
|
123
|
+
),
|
|
124
|
+
Intent(
|
|
125
|
+
intent_value="Bonjour, comment vas-tu?",
|
|
126
|
+
intent_type=IntentType.RAW,
|
|
127
|
+
intent_target="Bonjour, je vais bien merci, comment puis-je vous aider aujourd'hui?",
|
|
128
|
+
intent_scope=IntentScope.DIRECT,
|
|
129
|
+
),
|
|
130
|
+
Intent(
|
|
131
|
+
intent_value="Salut, ça va?",
|
|
132
|
+
intent_type=IntentType.RAW,
|
|
133
|
+
intent_target="Bonjour, je vais bien merci, comment puis-je vous aider aujourd'hui?",
|
|
134
|
+
intent_scope=IntentScope.DIRECT,
|
|
135
|
+
),
|
|
136
|
+
Intent(
|
|
137
|
+
intent_value="Thank you",
|
|
138
|
+
intent_type=IntentType.RAW,
|
|
139
|
+
intent_target="You're welcome, can I help you with anything else?",
|
|
140
|
+
intent_scope=IntentScope.DIRECT,
|
|
141
|
+
),
|
|
142
|
+
Intent(
|
|
143
|
+
intent_value="Thank you very much",
|
|
144
|
+
intent_type=IntentType.RAW,
|
|
145
|
+
intent_target="You're welcome, can I help you with anything else?",
|
|
146
|
+
intent_scope=IntentScope.DIRECT,
|
|
147
|
+
),
|
|
148
|
+
Intent(
|
|
149
|
+
intent_value="Thank you so much",
|
|
150
|
+
intent_type=IntentType.RAW,
|
|
151
|
+
intent_target="You're welcome, can I help you with anything else?",
|
|
152
|
+
intent_scope=IntentScope.DIRECT,
|
|
153
|
+
),
|
|
154
|
+
Intent(
|
|
155
|
+
intent_value="Merci",
|
|
156
|
+
intent_type=IntentType.RAW,
|
|
157
|
+
intent_target="Je vous en prie, puis-je vous aider avec autre chose?",
|
|
158
|
+
intent_scope=IntentScope.DIRECT,
|
|
159
|
+
),
|
|
160
|
+
Intent(
|
|
161
|
+
intent_value="Merci beaucoup",
|
|
162
|
+
intent_type=IntentType.RAW,
|
|
163
|
+
intent_target="Je vous en prie, puis-je vous aider avec autre chose?",
|
|
164
|
+
intent_scope=IntentScope.DIRECT,
|
|
165
|
+
),
|
|
166
|
+
Intent(
|
|
167
|
+
intent_value="Merci bien",
|
|
168
|
+
intent_type=IntentType.RAW,
|
|
169
|
+
intent_target="Je vous en prie, puis-je vous aider avec autre chose?",
|
|
170
|
+
intent_scope=IntentScope.DIRECT,
|
|
171
|
+
),
|
|
172
|
+
Intent(
|
|
173
|
+
intent_value="List tools available",
|
|
174
|
+
intent_type=IntentType.TOOL,
|
|
175
|
+
intent_target="list_tools_available",
|
|
176
|
+
intent_scope=IntentScope.DIRECT,
|
|
177
|
+
),
|
|
178
|
+
Intent(
|
|
179
|
+
intent_value="What are the tools available?",
|
|
180
|
+
intent_type=IntentType.TOOL,
|
|
181
|
+
intent_target="list_tools_available",
|
|
182
|
+
intent_scope=IntentScope.DIRECT,
|
|
183
|
+
),
|
|
184
|
+
Intent(
|
|
185
|
+
intent_value="List sub-agents available",
|
|
186
|
+
intent_type=IntentType.TOOL,
|
|
187
|
+
intent_target="list_subagents_available",
|
|
188
|
+
intent_scope=IntentScope.DIRECT,
|
|
189
|
+
),
|
|
190
|
+
Intent(
|
|
191
|
+
intent_value="What are the sub-agents available?",
|
|
192
|
+
intent_type=IntentType.TOOL,
|
|
193
|
+
intent_target="list_subagents_available",
|
|
194
|
+
intent_scope=IntentScope.DIRECT,
|
|
195
|
+
),
|
|
196
|
+
Intent(
|
|
197
|
+
intent_value="List intents",
|
|
198
|
+
intent_type=IntentType.TOOL,
|
|
199
|
+
intent_target="list_intents_available",
|
|
200
|
+
intent_scope=IntentScope.DIRECT,
|
|
201
|
+
),
|
|
202
|
+
Intent(
|
|
203
|
+
intent_value="What are your intents?",
|
|
204
|
+
intent_type=IntentType.TOOL,
|
|
205
|
+
intent_target="list_intents_available",
|
|
206
|
+
intent_scope=IntentScope.DIRECT,
|
|
207
|
+
),
|
|
208
|
+
]
|
|
209
|
+
DEV_INTENTS: list = [
|
|
210
|
+
# General development questions
|
|
211
|
+
Intent(
|
|
212
|
+
intent_value="How do I start developing on ABI?",
|
|
213
|
+
intent_type=IntentType.TOOL,
|
|
214
|
+
intent_target="read_makefile",
|
|
215
|
+
intent_scope=IntentScope.DIRECT,
|
|
216
|
+
),
|
|
217
|
+
Intent(
|
|
218
|
+
intent_value="What are the available make commands?",
|
|
219
|
+
intent_type=IntentType.TOOL,
|
|
220
|
+
intent_target="read_makefile",
|
|
221
|
+
intent_scope=IntentScope.DIRECT,
|
|
222
|
+
),
|
|
223
|
+
Intent(
|
|
224
|
+
intent_value="Show me all make commands",
|
|
225
|
+
intent_type=IntentType.TOOL,
|
|
226
|
+
intent_target="read_makefile",
|
|
227
|
+
intent_scope=IntentScope.DIRECT,
|
|
228
|
+
),
|
|
229
|
+
# Environment setup
|
|
230
|
+
Intent(
|
|
231
|
+
intent_value="How do I set up my development environment?",
|
|
232
|
+
intent_type=IntentType.TOOL,
|
|
233
|
+
intent_target="read_makefile",
|
|
234
|
+
intent_scope=IntentScope.DIRECT,
|
|
235
|
+
),
|
|
236
|
+
Intent(
|
|
237
|
+
intent_value="How do I install dependencies?",
|
|
238
|
+
intent_type=IntentType.TOOL,
|
|
239
|
+
intent_target="read_makefile",
|
|
240
|
+
intent_scope=IntentScope.DIRECT,
|
|
241
|
+
),
|
|
242
|
+
Intent(
|
|
243
|
+
intent_value="How do I create a virtual environment?",
|
|
244
|
+
intent_type=IntentType.TOOL,
|
|
245
|
+
intent_target="read_makefile",
|
|
246
|
+
intent_scope=IntentScope.DIRECT,
|
|
247
|
+
),
|
|
248
|
+
# Testing
|
|
249
|
+
Intent(
|
|
250
|
+
intent_value="How do I run tests?",
|
|
251
|
+
intent_type=IntentType.TOOL,
|
|
252
|
+
intent_target="read_makefile",
|
|
253
|
+
intent_scope=IntentScope.DIRECT,
|
|
254
|
+
),
|
|
255
|
+
Intent(
|
|
256
|
+
intent_value="How do I check code quality?",
|
|
257
|
+
intent_type=IntentType.TOOL,
|
|
258
|
+
intent_target="read_makefile",
|
|
259
|
+
intent_scope=IntentScope.DIRECT,
|
|
260
|
+
),
|
|
261
|
+
Intent(
|
|
262
|
+
intent_value="How do I run security scans?",
|
|
263
|
+
intent_type=IntentType.TOOL,
|
|
264
|
+
intent_target="read_makefile",
|
|
265
|
+
intent_scope=IntentScope.DIRECT,
|
|
266
|
+
),
|
|
267
|
+
# Docker operations
|
|
268
|
+
Intent(
|
|
269
|
+
intent_value="How do I work with Docker containers?",
|
|
270
|
+
intent_type=IntentType.TOOL,
|
|
271
|
+
intent_target="read_makefile",
|
|
272
|
+
intent_scope=IntentScope.DIRECT,
|
|
273
|
+
),
|
|
274
|
+
Intent(
|
|
275
|
+
intent_value="How do I start local services?",
|
|
276
|
+
intent_type=IntentType.TOOL,
|
|
277
|
+
intent_target="read_makefile",
|
|
278
|
+
intent_scope=IntentScope.DIRECT,
|
|
279
|
+
),
|
|
280
|
+
Intent(
|
|
281
|
+
intent_value="How do I manage Docker containers?",
|
|
282
|
+
intent_type=IntentType.TOOL,
|
|
283
|
+
intent_target="read_makefile",
|
|
284
|
+
intent_scope=IntentScope.DIRECT,
|
|
285
|
+
),
|
|
286
|
+
# Chat agents
|
|
287
|
+
Intent(
|
|
288
|
+
intent_value="What chat agents are available?",
|
|
289
|
+
intent_type=IntentType.TOOL,
|
|
290
|
+
intent_target="read_makefile",
|
|
291
|
+
intent_scope=IntentScope.DIRECT,
|
|
292
|
+
),
|
|
293
|
+
# Development tools
|
|
294
|
+
Intent(
|
|
295
|
+
intent_value="How do I use the API server?",
|
|
296
|
+
intent_type=IntentType.TOOL,
|
|
297
|
+
intent_target="read_makefile",
|
|
298
|
+
intent_scope=IntentScope.DIRECT,
|
|
299
|
+
),
|
|
300
|
+
Intent(
|
|
301
|
+
intent_value="How do I work with the knowledge graph?",
|
|
302
|
+
intent_type=IntentType.TOOL,
|
|
303
|
+
intent_target="read_makefile",
|
|
304
|
+
intent_scope=IntentScope.DIRECT,
|
|
305
|
+
),
|
|
306
|
+
Intent(
|
|
307
|
+
intent_value="How do I use Dagster for data orchestration?",
|
|
308
|
+
intent_type=IntentType.TOOL,
|
|
309
|
+
intent_target="read_makefile",
|
|
310
|
+
intent_scope=IntentScope.DIRECT,
|
|
311
|
+
),
|
|
312
|
+
# Module creation
|
|
313
|
+
Intent(
|
|
314
|
+
intent_value="How do I create new modules?",
|
|
315
|
+
intent_type=IntentType.TOOL,
|
|
316
|
+
intent_target="read_makefile",
|
|
317
|
+
intent_scope=IntentScope.DIRECT,
|
|
318
|
+
),
|
|
319
|
+
Intent(
|
|
320
|
+
intent_value="How do I create new agents?",
|
|
321
|
+
intent_type=IntentType.TOOL,
|
|
322
|
+
intent_target="read_makefile",
|
|
323
|
+
intent_scope=IntentScope.DIRECT,
|
|
324
|
+
),
|
|
325
|
+
Intent(
|
|
326
|
+
intent_value="How do I create new integrations?",
|
|
327
|
+
intent_type=IntentType.TOOL,
|
|
328
|
+
intent_target="read_makefile",
|
|
329
|
+
intent_scope=IntentScope.DIRECT,
|
|
330
|
+
),
|
|
331
|
+
Intent(
|
|
332
|
+
intent_value="How do I create new workflows?",
|
|
333
|
+
intent_type=IntentType.TOOL,
|
|
334
|
+
intent_target="read_makefile",
|
|
335
|
+
intent_scope=IntentScope.DIRECT,
|
|
336
|
+
),
|
|
337
|
+
Intent(
|
|
338
|
+
intent_value="How do I create new pipelines?",
|
|
339
|
+
intent_type=IntentType.TOOL,
|
|
340
|
+
intent_target="read_makefile",
|
|
341
|
+
intent_scope=IntentScope.DIRECT,
|
|
342
|
+
),
|
|
343
|
+
Intent(
|
|
344
|
+
intent_value="How do I create new ontologies?",
|
|
345
|
+
intent_type=IntentType.TOOL,
|
|
346
|
+
intent_target="read_makefile",
|
|
347
|
+
intent_scope=IntentScope.DIRECT,
|
|
348
|
+
),
|
|
349
|
+
# Data management
|
|
350
|
+
Intent(
|
|
351
|
+
intent_value="How do I manage data storage?",
|
|
352
|
+
intent_type=IntentType.TOOL,
|
|
353
|
+
intent_target="read_makefile",
|
|
354
|
+
intent_scope=IntentScope.DIRECT,
|
|
355
|
+
),
|
|
356
|
+
Intent(
|
|
357
|
+
intent_value="How do I work with the triplestore?",
|
|
358
|
+
intent_type=IntentType.TOOL,
|
|
359
|
+
intent_target="read_makefile",
|
|
360
|
+
intent_scope=IntentScope.DIRECT,
|
|
361
|
+
),
|
|
362
|
+
Intent(
|
|
363
|
+
intent_value="How do I handle data exports?",
|
|
364
|
+
intent_type=IntentType.TOOL,
|
|
365
|
+
intent_target="read_makefile",
|
|
366
|
+
intent_scope=IntentScope.DIRECT,
|
|
367
|
+
),
|
|
368
|
+
# Documentation
|
|
369
|
+
Intent(
|
|
370
|
+
intent_value="How do I generate documentation?",
|
|
371
|
+
intent_type=IntentType.TOOL,
|
|
372
|
+
intent_target="read_makefile",
|
|
373
|
+
intent_scope=IntentScope.DIRECT,
|
|
374
|
+
),
|
|
375
|
+
Intent(
|
|
376
|
+
intent_value="How do I publish agents?",
|
|
377
|
+
intent_type=IntentType.TOOL,
|
|
378
|
+
intent_target="read_makefile",
|
|
379
|
+
intent_scope=IntentScope.DIRECT,
|
|
380
|
+
),
|
|
381
|
+
# Cleanup and maintenance
|
|
382
|
+
Intent(
|
|
383
|
+
intent_value="How do I clean up the project?",
|
|
384
|
+
intent_type=IntentType.TOOL,
|
|
385
|
+
intent_target="read_makefile",
|
|
386
|
+
intent_scope=IntentScope.DIRECT,
|
|
387
|
+
),
|
|
388
|
+
Intent(
|
|
389
|
+
intent_value="How do I handle Docker cleanup?",
|
|
390
|
+
intent_type=IntentType.TOOL,
|
|
391
|
+
intent_target="read_makefile",
|
|
392
|
+
intent_scope=IntentScope.DIRECT,
|
|
393
|
+
),
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class IntentState(MessagesState):
|
|
398
|
+
"""State class for intent-based conversations.
|
|
399
|
+
|
|
400
|
+
Extends MessagesState to include intent mapping information that tracks
|
|
401
|
+
the current intent analysis results throughout the conversation flow.
|
|
402
|
+
|
|
403
|
+
Attributes:
|
|
404
|
+
intent_mapping (dict[str, Any]): Dictionary containing mapped intents
|
|
405
|
+
and their associated metadata from the intent analysis process.
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
intent_mapping: Dict[str, Any]
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class IntentAgent(Agent):
|
|
412
|
+
"""Agent with intent mapping and routing capabilities.
|
|
413
|
+
|
|
414
|
+
IntentAgent extends the base Agent class to provide intent-based conversation
|
|
415
|
+
routing. It analyzes user messages to identify and map them to predefined
|
|
416
|
+
intents, then routes the conversation flow accordingly. The agent includes
|
|
417
|
+
sophisticated filtering mechanisms for intent accuracy and entity validation.
|
|
418
|
+
|
|
419
|
+
The agent operates through several stages:
|
|
420
|
+
1. Intent mapping - Maps user messages to potential intents
|
|
421
|
+
2. Intent filtering - Filters out irrelevant intents
|
|
422
|
+
3. Entity checking - Validates entity consistency
|
|
423
|
+
4. Intent routing - Routes to appropriate handlers
|
|
424
|
+
|
|
425
|
+
Attributes:
|
|
426
|
+
_intents (list[Intent]): List of available intents for mapping
|
|
427
|
+
_intent_mapper (IntentMapper): Mapper instance for intent analysis
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
_intents: list[Intent]
|
|
431
|
+
_intent_mapper: IntentMapper
|
|
432
|
+
|
|
433
|
+
def __init__(
|
|
434
|
+
self,
|
|
435
|
+
name: str,
|
|
436
|
+
description: str,
|
|
437
|
+
chat_model: BaseChatModel | ChatModel,
|
|
438
|
+
tools: list[Union[Tool, BaseTool, "Agent"]] = [],
|
|
439
|
+
agents: list["Agent"] = [],
|
|
440
|
+
intents: list[Intent] = [],
|
|
441
|
+
memory: BaseCheckpointSaver | None = None,
|
|
442
|
+
state: AgentSharedState = AgentSharedState(),
|
|
443
|
+
configuration: AgentConfiguration = AgentConfiguration(),
|
|
444
|
+
event_queue: Queue | None = None,
|
|
445
|
+
threshold: float = 0.85,
|
|
446
|
+
threshold_neighbor: float = 0.05,
|
|
447
|
+
direct_intent_score: float = 0.95,
|
|
448
|
+
):
|
|
449
|
+
"""Initialize the IntentAgent.
|
|
450
|
+
|
|
451
|
+
Sets up the agent with intent mapping capabilities by initializing
|
|
452
|
+
the intent mapper before calling the parent constructor.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
name (str): Unique name identifier for the agent
|
|
456
|
+
description (str): Human-readable description of the agent's purpose
|
|
457
|
+
chat_model (BaseChatModel): Language model for generating responses
|
|
458
|
+
tools (list[Union[Tool, "Agent"]], optional): Available tools and sub-agents.
|
|
459
|
+
Defaults to [].
|
|
460
|
+
agents (list["Agent"], optional): List of sub-agents this agent can route to.
|
|
461
|
+
Defaults to [].
|
|
462
|
+
intents (list[Intent], optional): List of intents for mapping user messages.
|
|
463
|
+
Defaults to [].
|
|
464
|
+
memory (BaseCheckpointSaver | None, optional): Checkpoint saver for conversation state.
|
|
465
|
+
If None, will use PostgreSQL if POSTGRES_URL env var is set, otherwise in-memory.
|
|
466
|
+
Defaults to None.
|
|
467
|
+
state (AgentSharedState, optional): Shared state configuration.
|
|
468
|
+
Defaults to AgentSharedState().
|
|
469
|
+
configuration (AgentConfiguration, optional): Agent configuration settings.
|
|
470
|
+
Defaults to AgentConfiguration().
|
|
471
|
+
event_queue (Queue | None, optional): Queue for handling events.
|
|
472
|
+
Defaults to None.
|
|
473
|
+
threshold (float, optional): Minimum score threshold for intent matching.
|
|
474
|
+
Defaults to 0.85.
|
|
475
|
+
threshold_neighbor (float, optional): Maximum score difference for similar intents.
|
|
476
|
+
Defaults to 0.05.
|
|
477
|
+
"""
|
|
478
|
+
# Add default intents while avoiding duplicates
|
|
479
|
+
intent_values = {
|
|
480
|
+
(intent.intent_value, intent.intent_type, intent.intent_target)
|
|
481
|
+
for intent in intents
|
|
482
|
+
}
|
|
483
|
+
for default_intent in DEFAULT_INTENTS:
|
|
484
|
+
if (
|
|
485
|
+
default_intent.intent_value,
|
|
486
|
+
default_intent.intent_type,
|
|
487
|
+
default_intent.intent_target,
|
|
488
|
+
) not in intent_values:
|
|
489
|
+
intents.append(default_intent)
|
|
490
|
+
|
|
491
|
+
if os.environ.get("ENV") != "prod":
|
|
492
|
+
for dev_intent in DEV_INTENTS:
|
|
493
|
+
if (
|
|
494
|
+
dev_intent.intent_value,
|
|
495
|
+
dev_intent.intent_type,
|
|
496
|
+
dev_intent.intent_target,
|
|
497
|
+
) not in intent_values:
|
|
498
|
+
intents.append(dev_intent)
|
|
499
|
+
|
|
500
|
+
self._intents = intents
|
|
501
|
+
self._intent_mapper = IntentMapper(self._intents)
|
|
502
|
+
self._threshold = threshold
|
|
503
|
+
self._threshold_neighbor = threshold_neighbor
|
|
504
|
+
self._direct_intent_score = direct_intent_score
|
|
505
|
+
|
|
506
|
+
# Handle memory configuration (same pattern as base Agent class)
|
|
507
|
+
if memory is None:
|
|
508
|
+
memory = create_checkpointer()
|
|
509
|
+
|
|
510
|
+
super().__init__(
|
|
511
|
+
name=name,
|
|
512
|
+
description=description,
|
|
513
|
+
chat_model=chat_model,
|
|
514
|
+
tools=tools,
|
|
515
|
+
agents=agents,
|
|
516
|
+
memory=memory,
|
|
517
|
+
state=state,
|
|
518
|
+
configuration=configuration,
|
|
519
|
+
event_queue=event_queue,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
@property
|
|
523
|
+
def intents(self) -> list[Intent]:
|
|
524
|
+
return self._intents
|
|
525
|
+
|
|
526
|
+
def build_graph(self, patcher: Optional[Callable] = None):
|
|
527
|
+
"""Build the conversation flow graph for the IntentAgent.
|
|
528
|
+
|
|
529
|
+
Constructs a StateGraph that defines the conversation flow with intent
|
|
530
|
+
mapping capabilities. The graph includes nodes for intent mapping,
|
|
531
|
+
filtering, entity checking, routing, and integration with sub-agents.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
patcher (Optional[Callable], optional): Optional function to modify
|
|
535
|
+
the graph before compilation. Defaults to None.
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
None: Sets the compiled graph on the agent instance
|
|
539
|
+
"""
|
|
540
|
+
graph = StateGraph(IntentState)
|
|
541
|
+
|
|
542
|
+
graph.add_node(self.current_active_agent)
|
|
543
|
+
graph.add_edge(START, "current_active_agent")
|
|
544
|
+
|
|
545
|
+
graph.add_node(self.continue_conversation)
|
|
546
|
+
|
|
547
|
+
graph.add_node(self.map_intents)
|
|
548
|
+
|
|
549
|
+
graph.add_node(self.filter_out_intents)
|
|
550
|
+
|
|
551
|
+
graph.add_node(self.entity_check)
|
|
552
|
+
|
|
553
|
+
graph.add_node(self.intent_mapping_router)
|
|
554
|
+
graph.add_edge("entity_check", "intent_mapping_router")
|
|
555
|
+
|
|
556
|
+
graph.add_node(self.request_human_validation)
|
|
557
|
+
graph.add_edge("request_human_validation", END)
|
|
558
|
+
|
|
559
|
+
graph.add_node(self.inject_intents_in_system_prompt)
|
|
560
|
+
graph.add_edge("inject_intents_in_system_prompt", "call_model")
|
|
561
|
+
|
|
562
|
+
graph.add_node(self.call_model)
|
|
563
|
+
graph.add_edge("call_model", END)
|
|
564
|
+
|
|
565
|
+
graph.add_node(self.call_tools)
|
|
566
|
+
|
|
567
|
+
for agent in self._agents:
|
|
568
|
+
graph.add_node(agent.name, agent.graph)
|
|
569
|
+
|
|
570
|
+
if patcher is not None:
|
|
571
|
+
graph = patcher(graph)
|
|
572
|
+
|
|
573
|
+
self.graph = graph.compile(checkpointer=self._checkpointer)
|
|
574
|
+
|
|
575
|
+
def continue_conversation(self, state: MessagesState) -> Command:
|
|
576
|
+
return Command(goto="map_intents")
|
|
577
|
+
|
|
578
|
+
def map_intents(self, state: IntentState) -> Command:
|
|
579
|
+
"""Map user messages to available intents.
|
|
580
|
+
|
|
581
|
+
Analyzes the last human message to identify matching intents using
|
|
582
|
+
vector similarity search. Handles special cases like @ mentions for
|
|
583
|
+
direct agent routing. Performs initial intent filtering based on
|
|
584
|
+
confidence scores.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
state (MessagesState): Current conversation state containing messages
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
Command | dict: Either a Command to route to a specific agent (for @ mentions)
|
|
591
|
+
or a state update dictionary containing mapped intents
|
|
592
|
+
|
|
593
|
+
Raises:
|
|
594
|
+
AssertionError: If no human message is found in the conversation state
|
|
595
|
+
"""
|
|
596
|
+
# Reset intents rules in system prompt
|
|
597
|
+
# self._system_prompt = self._configuration.system_prompt
|
|
598
|
+
|
|
599
|
+
# Get the last messages
|
|
600
|
+
last_ai_message: Any | None = pd.find(
|
|
601
|
+
state["messages"][::-1], lambda m: isinstance(m, AIMessage)
|
|
602
|
+
)
|
|
603
|
+
last_human_message = self.get_last_human_message(state)
|
|
604
|
+
|
|
605
|
+
# Assertions to ensure the last human message is valid
|
|
606
|
+
assert last_human_message is not None
|
|
607
|
+
assert isinstance(last_human_message, HumanMessage)
|
|
608
|
+
assert isinstance(last_human_message.content, str)
|
|
609
|
+
|
|
610
|
+
logger.debug("🔍 Map intents")
|
|
611
|
+
logger.debug(
|
|
612
|
+
f"==> Last human message: {last_human_message.content if last_human_message is not None else None}"
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# Handle multiples intents routing via numeric response to a validation request (e.g., "1", "2", etc.)
|
|
616
|
+
if (
|
|
617
|
+
isinstance(last_human_message.content, str)
|
|
618
|
+
and last_human_message.content.strip().isdigit()
|
|
619
|
+
and last_ai_message is not None
|
|
620
|
+
and MULTIPLES_INTENTS_MESSAGE in last_ai_message.content
|
|
621
|
+
and last_ai_message.additional_kwargs.get("owner") == self.name
|
|
622
|
+
):
|
|
623
|
+
choice_num = int(last_human_message.content.strip())
|
|
624
|
+
|
|
625
|
+
logger.debug(
|
|
626
|
+
"🔀 Handle multiples intents routing via numeric response to a validation request (e.g., '1', '2', etc.)"
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
# Extract agent names from the validation message
|
|
630
|
+
lines = last_ai_message.content.split("\n")
|
|
631
|
+
intent_lines = [
|
|
632
|
+
line for line in lines if line.strip().startswith(f"{choice_num}.")
|
|
633
|
+
]
|
|
634
|
+
if intent_lines:
|
|
635
|
+
command_update: dict = {"intent_mapping": {"intents": []}}
|
|
636
|
+
logger.debug(f"Command update: {command_update}")
|
|
637
|
+
|
|
638
|
+
# Extract agent name from the line like "1. **AgentName** (confidence: 89.7%)"
|
|
639
|
+
intent_line = intent_lines[0]
|
|
640
|
+
if "**" in intent_line:
|
|
641
|
+
intent_name = intent_line.split("**")[1]
|
|
642
|
+
agent = pd.find(self._agents, lambda a: a.name == intent_name)
|
|
643
|
+
if agent is not None:
|
|
644
|
+
logger.debug(f"✅ Calling agent: {intent_name}")
|
|
645
|
+
self.state.set_current_active_agent(intent_name)
|
|
646
|
+
return Command(goto=intent_name, update=command_update)
|
|
647
|
+
else:
|
|
648
|
+
logger.debug("❌ Agent not found, going to call_model")
|
|
649
|
+
return Command(goto="call_model", update=command_update)
|
|
650
|
+
|
|
651
|
+
# Map intents using vector similarity search
|
|
652
|
+
intents = pd.filter_(
|
|
653
|
+
self._intent_mapper.map_intent(last_human_message.content, k=10),
|
|
654
|
+
lambda matches: matches["score"] > self._threshold,
|
|
655
|
+
)
|
|
656
|
+
if len(intents) == 0:
|
|
657
|
+
_, prompted_intents = self._intent_mapper.map_prompt(
|
|
658
|
+
last_human_message.content, k=10
|
|
659
|
+
)
|
|
660
|
+
intents = pd.filter_(
|
|
661
|
+
prompted_intents, lambda matches: matches["score"] > self._threshold
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
# If we have no intents, we return an empty intent mapping
|
|
665
|
+
if len(intents) == 0:
|
|
666
|
+
return Command(
|
|
667
|
+
update={"intent_mapping": {"intents": []}},
|
|
668
|
+
goto=self.should_filter([]),
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
# Keep intents that are close to the best intent.
|
|
672
|
+
max_score = intents[0]["score"]
|
|
673
|
+
max_score_2 = intents[1]["score"] if len(intents) > 1 else 0
|
|
674
|
+
if max_score >= self._direct_intent_score and max_score > max_score_2:
|
|
675
|
+
logger.debug(
|
|
676
|
+
f"🎯 Intent mapping score above {self._direct_intent_score * 100}% ({round(max_score * 100, 2)}%), routing to intent_mapping_router"
|
|
677
|
+
)
|
|
678
|
+
return Command(
|
|
679
|
+
goto="intent_mapping_router",
|
|
680
|
+
update={"intent_mapping": {"intents": [intents[0]]}},
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
close_intents = pd.filter_(
|
|
684
|
+
intents,
|
|
685
|
+
lambda intent: max_score - intent["score"] < self._threshold_neighbor,
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
assert isinstance(close_intents, list)
|
|
689
|
+
|
|
690
|
+
# Filter out intents with duplicate targets, keeping the highest scoring one
|
|
691
|
+
seen_targets = set()
|
|
692
|
+
final_intents = []
|
|
693
|
+
for intent in close_intents:
|
|
694
|
+
target = intent["intent"].intent_target
|
|
695
|
+
if target not in seen_targets:
|
|
696
|
+
seen_targets.add(target)
|
|
697
|
+
final_intents.append(intent)
|
|
698
|
+
|
|
699
|
+
logger.debug(f"{len(final_intents)} intents mapped: {final_intents}")
|
|
700
|
+
state["intent_mapping"] = {"intents": final_intents}
|
|
701
|
+
return Command(
|
|
702
|
+
goto=self.should_filter(final_intents),
|
|
703
|
+
update={"intent_mapping": {"intents": final_intents}},
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
def should_filter(self, intents: list) -> str:
|
|
707
|
+
"""Determine if intent mapping should be filtered.
|
|
708
|
+
|
|
709
|
+
Checks if the intent mapping should be filtered based on the threshold
|
|
710
|
+
and neighbor values.
|
|
711
|
+
"""
|
|
712
|
+
if len(intents) == 1 and intents[0]["score"] > self._direct_intent_score:
|
|
713
|
+
return "intent_mapping_router"
|
|
714
|
+
if len(intents) == 0:
|
|
715
|
+
logger.debug("❌ No intents found, going to call_model")
|
|
716
|
+
return "call_model"
|
|
717
|
+
|
|
718
|
+
return "filter_out_intents"
|
|
719
|
+
|
|
720
|
+
def filter_out_intents(self, state: IntentState) -> Command:
|
|
721
|
+
"""Filter out logically irrelevant intents using LLM analysis.
|
|
722
|
+
|
|
723
|
+
Uses the chat model to analyze mapped intents and filter out those that
|
|
724
|
+
are not logically compatible with the user's message. This addresses
|
|
725
|
+
cases where vector similarity alone may match irrelevant intents.
|
|
726
|
+
|
|
727
|
+
Args:
|
|
728
|
+
state (dict[str, Any]): Current conversation state with mapped intents
|
|
729
|
+
|
|
730
|
+
Returns:
|
|
731
|
+
Command: Command to update state with filtered intents
|
|
732
|
+
if no filtering is needed
|
|
733
|
+
"""
|
|
734
|
+
logger.debug("🔍 Filter out irrelevant intents")
|
|
735
|
+
last_human_message = self.get_last_human_message(state)
|
|
736
|
+
assert last_human_message is not None
|
|
737
|
+
|
|
738
|
+
mapped_intents = state["intent_mapping"]["intents"]
|
|
739
|
+
|
|
740
|
+
@tool
|
|
741
|
+
def filter_intents(bool_list: list[bool]) -> list[Intent]:
|
|
742
|
+
"""
|
|
743
|
+
This tool is used to filter out the intents that are not related to the last user message. True will keep the intent, false will remove it.
|
|
744
|
+
"""
|
|
745
|
+
return []
|
|
746
|
+
|
|
747
|
+
intents = [intent["text"] for intent in mapped_intents]
|
|
748
|
+
|
|
749
|
+
system_prompt = f"""You are a logical assistant. You are given a list of possible intents retrieved via vector search from the last user message. These matches may not always be logically relevant because the search is based only on surface similarity, without full understanding of the request.
|
|
750
|
+
|
|
751
|
+
Your task is to filter out any intent that is not logically compatible with the last user message.
|
|
752
|
+
|
|
753
|
+
You must examine whether the user’s message and the mapped intent match **in meaning and logical structure**. You should exclude intents where:
|
|
754
|
+
- The intent contains named entities (like people or organizations) that are not mentioned in the user message.
|
|
755
|
+
- The intent refers to actions or goals not implied by the user message.
|
|
756
|
+
- The intent is more specific than the user message in a way that changes the meaning (e.g., user asks for a general phone number but the intent asks for the phone number of a specific person).
|
|
757
|
+
- The intent cannot logically follow from the user message, even if some keywords are similar.
|
|
758
|
+
- The intent may be topically similar but not what the user is asking for.
|
|
759
|
+
|
|
760
|
+
You will be shown:
|
|
761
|
+
- The list of mapped intents
|
|
762
|
+
- The last user message
|
|
763
|
+
|
|
764
|
+
You must call the tool `filter_intents` once and only once with a list of booleans. Each boolean corresponds to whether the mapped intent at that index is logically relevant to the user message.
|
|
765
|
+
|
|
766
|
+
Be strict — include only intents that directly and logically correspond to the user's actual request.
|
|
767
|
+
|
|
768
|
+
Example:
|
|
769
|
+
- User says: "Give me the personal email address"
|
|
770
|
+
- Intent: "Give me the personal email address of John Doe" → This should be **excluded** (not logically equivalent, adds information not present in prompt)
|
|
771
|
+
|
|
772
|
+
Now, analyze and apply this reasoning to the intents.
|
|
773
|
+
|
|
774
|
+
Mapped intents:
|
|
775
|
+
```mapped_intents
|
|
776
|
+
{intents}
|
|
777
|
+
```
|
|
778
|
+
Last user message: "{last_human_message.content}"
|
|
779
|
+
"""
|
|
780
|
+
|
|
781
|
+
# messages = [SystemMessage(content=system_prompt), last_human_message]
|
|
782
|
+
messages: list = [SystemMessage(content=system_prompt)]
|
|
783
|
+
for message in state["messages"]:
|
|
784
|
+
if not isinstance(message, SystemMessage):
|
|
785
|
+
messages.append(message)
|
|
786
|
+
|
|
787
|
+
try:
|
|
788
|
+
response = self._chat_model.bind_tools([filter_intents]).invoke(messages)
|
|
789
|
+
except Exception:
|
|
790
|
+
logger.warning("Error filtering intents going to 'entity_check'")
|
|
791
|
+
return Command(goto="entity_check")
|
|
792
|
+
|
|
793
|
+
assert isinstance(response, AIMessage)
|
|
794
|
+
|
|
795
|
+
filtered_intents: list = []
|
|
796
|
+
|
|
797
|
+
try:
|
|
798
|
+
assert isinstance(response.tool_calls, list), response.tool_calls
|
|
799
|
+
assert len(response.tool_calls) > 0, response.tool_calls
|
|
800
|
+
assert isinstance(response.tool_calls[0]["args"], dict), response.tool_calls
|
|
801
|
+
assert "bool_list" in response.tool_calls[0]["args"], response.tool_calls
|
|
802
|
+
assert isinstance(response.tool_calls[0]["args"]["bool_list"], list), (
|
|
803
|
+
response.tool_calls
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
bool_list = response.tool_calls[0]["args"]["bool_list"]
|
|
807
|
+
for i in range(len(bool_list)):
|
|
808
|
+
if bool_list[i]:
|
|
809
|
+
filtered_intents.append(mapped_intents[i])
|
|
810
|
+
except Exception as e:
|
|
811
|
+
logger.error(f"Error filtering out intents: {e}")
|
|
812
|
+
filtered_intents = mapped_intents
|
|
813
|
+
|
|
814
|
+
logger.debug(f"{len(filtered_intents)} intents filtered: {filtered_intents}")
|
|
815
|
+
state["intent_mapping"] = {"intents": filtered_intents}
|
|
816
|
+
if (
|
|
817
|
+
len(filtered_intents) == 1
|
|
818
|
+
and filtered_intents[0]["score"] > self._threshold
|
|
819
|
+
):
|
|
820
|
+
return Command(
|
|
821
|
+
goto="intent_mapping_router",
|
|
822
|
+
update={"intent_mapping": {"intents": filtered_intents}},
|
|
823
|
+
)
|
|
824
|
+
return Command(
|
|
825
|
+
goto="entity_check",
|
|
826
|
+
update={"intent_mapping": {"intents": filtered_intents}},
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
def _extract_entities(self, text: str) -> list[str]:
|
|
830
|
+
"""Extract named entities from text using spaCy.
|
|
831
|
+
|
|
832
|
+
Uses the spaCy NLP model to identify and extract named entities
|
|
833
|
+
from the provided text, returning them in lowercase for consistent
|
|
834
|
+
comparison.
|
|
835
|
+
|
|
836
|
+
Args:
|
|
837
|
+
text (str): Input text to extract entities from
|
|
838
|
+
|
|
839
|
+
Returns:
|
|
840
|
+
list[str]: List of extracted entity texts in lowercase
|
|
841
|
+
"""
|
|
842
|
+
doc = get_nlp()(text)
|
|
843
|
+
return [ent.text.lower() for ent in doc.ents]
|
|
844
|
+
|
|
845
|
+
def entity_check(self, state: IntentState) -> Command:
|
|
846
|
+
"""Validate entity consistency between user message and intents.
|
|
847
|
+
|
|
848
|
+
Performs entity checking to ensure that intents containing named entities
|
|
849
|
+
are only kept if those entities are present or implied in the user's
|
|
850
|
+
message. Uses both rule-based and LLM-based validation approaches.
|
|
851
|
+
|
|
852
|
+
Args:
|
|
853
|
+
state (dict[str, Any]): Current conversation state with mapped intents
|
|
854
|
+
|
|
855
|
+
Returns:
|
|
856
|
+
Command | None: Command to update state with entity-validated intents,
|
|
857
|
+
or None if no intent mapping exists
|
|
858
|
+
"""
|
|
859
|
+
logger.debug("🔍 Entity Check")
|
|
860
|
+
|
|
861
|
+
last_human_message = self.get_last_human_message(state)
|
|
862
|
+
|
|
863
|
+
assert last_human_message is not None
|
|
864
|
+
assert isinstance(last_human_message, HumanMessage)
|
|
865
|
+
assert isinstance(last_human_message.content, str)
|
|
866
|
+
assert "intent_mapping" in state
|
|
867
|
+
|
|
868
|
+
mapped_intents = state["intent_mapping"]["intents"]
|
|
869
|
+
|
|
870
|
+
filtered_intents = []
|
|
871
|
+
|
|
872
|
+
for intent in mapped_intents:
|
|
873
|
+
entities = self._extract_entities(intent["intent"].intent_value)
|
|
874
|
+
|
|
875
|
+
if len(entities) == 0:
|
|
876
|
+
filtered_intents.append(intent)
|
|
877
|
+
continue
|
|
878
|
+
|
|
879
|
+
last_human_message_entities = self._extract_entities(
|
|
880
|
+
last_human_message.content
|
|
881
|
+
)
|
|
882
|
+
all_entities_present = False
|
|
883
|
+
if len(last_human_message_entities) > 0 and len(
|
|
884
|
+
last_human_message_entities
|
|
885
|
+
) >= len(entities):
|
|
886
|
+
all_entities_present = True
|
|
887
|
+
for entity in last_human_message_entities:
|
|
888
|
+
if entity not in entities:
|
|
889
|
+
all_entities_present = False
|
|
890
|
+
break
|
|
891
|
+
|
|
892
|
+
if all_entities_present:
|
|
893
|
+
filtered_intents.append(intent)
|
|
894
|
+
continue
|
|
895
|
+
|
|
896
|
+
messages: list[BaseMessage] = [
|
|
897
|
+
SystemMessage(
|
|
898
|
+
content=f"""
|
|
899
|
+
You are a precise and logical assistant. You will be given:
|
|
900
|
+
- An **intent** (which includes one or more named entities)
|
|
901
|
+
- A list of **entities** extracted from that intent
|
|
902
|
+
- The **last user message**
|
|
903
|
+
- The **chat history**
|
|
904
|
+
|
|
905
|
+
Your task is to determine whether the last user message (and optionally the conversation history) clearly **refers to or requests information about** the entities in the intent.
|
|
906
|
+
|
|
907
|
+
You must answer **"true"** if:
|
|
908
|
+
- The user's message explicitly or implicitly refers to **all** the key entities in the intent (such as a specific person, object, or organization).
|
|
909
|
+
- The user's request logically aligns with the target entities (e.g., same person, role, or context).
|
|
910
|
+
|
|
911
|
+
Answer **"false"** if:
|
|
912
|
+
- The user’s message does **not mention** or clearly imply the entities.
|
|
913
|
+
- The intent introduces **entities that were not referenced** in the user's message or recent chat history.
|
|
914
|
+
- There is insufficient information to link the user's request to those specific entities.
|
|
915
|
+
|
|
916
|
+
⚠️ Very Important:
|
|
917
|
+
- You must output **"true"** or **"false"** only. No explanations. No other words.
|
|
918
|
+
- Your answer will be parsed by a test function and must strictly match one of those two strings.
|
|
919
|
+
|
|
920
|
+
### Example:
|
|
921
|
+
Intent: "What is the color of the dress of Lucie?"
|
|
922
|
+
Entities: ["Lucie", "dress"]
|
|
923
|
+
Last user message: "What is the color of the dress of Lucie?"
|
|
924
|
+
Chat history: ["What is the color of the dress of Lucie?", "The color of the dress of Lucie is blue"]
|
|
925
|
+
Output: "true"
|
|
926
|
+
|
|
927
|
+
Now analyze the following:
|
|
928
|
+
|
|
929
|
+
Entities: {entities}
|
|
930
|
+
Last user message: "{last_human_message.content}"
|
|
931
|
+
"""
|
|
932
|
+
)
|
|
933
|
+
]
|
|
934
|
+
|
|
935
|
+
for message in state["messages"]:
|
|
936
|
+
if isinstance(message, HumanMessage):
|
|
937
|
+
messages.append(message)
|
|
938
|
+
|
|
939
|
+
response = self._chat_model.invoke(messages)
|
|
940
|
+
if response.content == "true":
|
|
941
|
+
filtered_intents.append(intent)
|
|
942
|
+
|
|
943
|
+
logger.debug(
|
|
944
|
+
f"{len(filtered_intents)} intents filtered after entity check: {filtered_intents}"
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
return Command(update={"intent_mapping": {"intents": filtered_intents}})
|
|
948
|
+
|
|
949
|
+
def request_human_validation(self, state: IntentState) -> Command:
|
|
950
|
+
"""Request human validation when multiple agents are above threshold.
|
|
951
|
+
|
|
952
|
+
When multiple agent intents are identified with scores above the threshold,
|
|
953
|
+
this method asks the human user to choose which agent they want to use.
|
|
954
|
+
It presents the available options and waits for user input.
|
|
955
|
+
|
|
956
|
+
Args:
|
|
957
|
+
state (IntentState): Current conversation state with multiple agent intents
|
|
958
|
+
|
|
959
|
+
Returns:
|
|
960
|
+
Command: Command to end conversation with validation request message
|
|
961
|
+
"""
|
|
962
|
+
logger.debug("👀 Request Human Validation")
|
|
963
|
+
|
|
964
|
+
if (
|
|
965
|
+
"intent_mapping" not in state
|
|
966
|
+
or len(state["intent_mapping"]["intents"]) == 0
|
|
967
|
+
):
|
|
968
|
+
return Command(goto="call_model")
|
|
969
|
+
|
|
970
|
+
agent_intents = [
|
|
971
|
+
intent
|
|
972
|
+
for intent in state["intent_mapping"]["intents"]
|
|
973
|
+
if intent["intent"].intent_type in [IntentType.AGENT, IntentType.TOOL]
|
|
974
|
+
]
|
|
975
|
+
|
|
976
|
+
if len(agent_intents) <= 1:
|
|
977
|
+
return Command(goto="inject_intents_in_system_prompt")
|
|
978
|
+
|
|
979
|
+
# Create a list of unique agents with their scores
|
|
980
|
+
agents_info = []
|
|
981
|
+
seen_agents = set()
|
|
982
|
+
|
|
983
|
+
for intent in agent_intents:
|
|
984
|
+
agent_name = intent["intent"].intent_target
|
|
985
|
+
if agent_name not in seen_agents:
|
|
986
|
+
agents_info.append(
|
|
987
|
+
{
|
|
988
|
+
"name": agent_name,
|
|
989
|
+
"score": intent["score"],
|
|
990
|
+
"intent_text": intent["intent"].intent_value,
|
|
991
|
+
}
|
|
992
|
+
)
|
|
993
|
+
seen_agents.add(agent_name)
|
|
994
|
+
|
|
995
|
+
# Sort by score (descending) and then by agent name (ascending)
|
|
996
|
+
agents_info.sort(key=lambda x: (-x["score"], x["name"]))
|
|
997
|
+
|
|
998
|
+
# Create the validation message
|
|
999
|
+
# validation_message = f"Validation Request: '{initial_human_message.content}'\n\n"
|
|
1000
|
+
validation_message = (
|
|
1001
|
+
"I found multiple intents that could handle your request:\n\n"
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
for i, agent_info in enumerate(agents_info, 1):
|
|
1005
|
+
validation_message += f"{i}. **{agent_info['name']}** (confidence: {agent_info['score']:.1%})\n"
|
|
1006
|
+
validation_message += f" Intent: {agent_info['intent_text']}\n\n"
|
|
1007
|
+
|
|
1008
|
+
validation_message += "Please choose an intent by number (e.g., '1' or '2')\n"
|
|
1009
|
+
|
|
1010
|
+
ai_message = AIMessage(
|
|
1011
|
+
content=validation_message, additional_kwargs={"owner": self.name}
|
|
1012
|
+
)
|
|
1013
|
+
self._notify_ai_message(ai_message, self.name)
|
|
1014
|
+
|
|
1015
|
+
return Command(goto=END, update={"messages": [ai_message]})
|
|
1016
|
+
|
|
1017
|
+
def intent_mapping_router(self, state: IntentState) -> Command:
|
|
1018
|
+
"""Route conversation flow based on intent mapping results.
|
|
1019
|
+
|
|
1020
|
+
Analyzes the current intent mapping state and determines the next step
|
|
1021
|
+
in the conversation flow. Routes to different handlers based on the
|
|
1022
|
+
number and type of mapped intents.
|
|
1023
|
+
|
|
1024
|
+
Args:
|
|
1025
|
+
state (dict[str, Any]): Current conversation state with intent mapping
|
|
1026
|
+
|
|
1027
|
+
Returns:
|
|
1028
|
+
Command: Command specifying the next node to execute in the graph
|
|
1029
|
+
"""
|
|
1030
|
+
logger.debug("🔀 Intent Mapping Router")
|
|
1031
|
+
|
|
1032
|
+
if "intent_mapping" in state:
|
|
1033
|
+
intent_mapping = state["intent_mapping"]
|
|
1034
|
+
# If there are no intents, we check if there's an active agent for context preservation
|
|
1035
|
+
if len(intent_mapping["intents"]) == 0:
|
|
1036
|
+
logger.debug("❌ No intents found, calling model for active agent")
|
|
1037
|
+
return Command(goto="call_model")
|
|
1038
|
+
|
|
1039
|
+
# If there's a single intent is mapped, we check the intent type to return the appropriate Command
|
|
1040
|
+
if len(intent_mapping["intents"]) == 1:
|
|
1041
|
+
logger.debug("✅ Single intent found, routing to appropriate handler")
|
|
1042
|
+
intent: Intent = intent_mapping["intents"][0]["intent"]
|
|
1043
|
+
if intent.intent_type == IntentType.RAW:
|
|
1044
|
+
logger.debug(
|
|
1045
|
+
f"📝 Raw intent found, routing to '{intent.intent_target}'"
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
ai_message = AIMessage(content=intent.intent_target)
|
|
1049
|
+
self._notify_ai_message(ai_message, self.name)
|
|
1050
|
+
|
|
1051
|
+
return Command(goto=END, update={"messages": [ai_message]})
|
|
1052
|
+
elif intent.intent_type == IntentType.AGENT:
|
|
1053
|
+
logger.debug(
|
|
1054
|
+
f"🤖 Agent intent found, routing to {intent.intent_target}"
|
|
1055
|
+
)
|
|
1056
|
+
if intent.intent_target != "call_model":
|
|
1057
|
+
self.state.set_current_active_agent(intent.intent_target)
|
|
1058
|
+
return Command(goto=intent.intent_target)
|
|
1059
|
+
else:
|
|
1060
|
+
logger.debug(
|
|
1061
|
+
"🔧 Tool intent found, routing to inject intents in system prompt"
|
|
1062
|
+
)
|
|
1063
|
+
return Command(goto="inject_intents_in_system_prompt")
|
|
1064
|
+
|
|
1065
|
+
# If there are multiple intents, we check intents type to return the appropriate Command
|
|
1066
|
+
elif len(intent_mapping["intents"]) > 1:
|
|
1067
|
+
logger.debug(
|
|
1068
|
+
"✅ Multiple intents found, routing to request human validation or inject intents in system prompt"
|
|
1069
|
+
)
|
|
1070
|
+
# Check if we have multiple agent intents above threshold
|
|
1071
|
+
not_raw_intents = [
|
|
1072
|
+
intent
|
|
1073
|
+
for intent in intent_mapping["intents"]
|
|
1074
|
+
if intent["intent"].intent_type
|
|
1075
|
+
in [IntentType.AGENT, IntentType.TOOL]
|
|
1076
|
+
]
|
|
1077
|
+
|
|
1078
|
+
if len(not_raw_intents) > 1:
|
|
1079
|
+
# We have multiple agent/tools intents - ask human for validation
|
|
1080
|
+
return Command(goto="request_human_validation")
|
|
1081
|
+
else:
|
|
1082
|
+
return Command(goto="inject_intents_in_system_prompt")
|
|
1083
|
+
|
|
1084
|
+
return Command(goto="call_model")
|
|
1085
|
+
|
|
1086
|
+
def inject_intents_in_system_prompt(self, state: IntentState):
|
|
1087
|
+
"""Inject mapped intents into the system prompt.
|
|
1088
|
+
|
|
1089
|
+
Updates the agent's system prompt to include information about
|
|
1090
|
+
mapped intents, providing context for the language model to
|
|
1091
|
+
make intent-aware responses.
|
|
1092
|
+
|
|
1093
|
+
Args:
|
|
1094
|
+
state (dict[str, Any]): Current conversation state with intent mapping
|
|
1095
|
+
|
|
1096
|
+
Returns:
|
|
1097
|
+
None: Updates the agent's system prompt in place
|
|
1098
|
+
"""
|
|
1099
|
+
logger.debug("💉 Inject Intents in System Prompt")
|
|
1100
|
+
|
|
1101
|
+
# We reset the system prompt to the original one.
|
|
1102
|
+
# self._system_prompt = self._configuration.system_prompt
|
|
1103
|
+
|
|
1104
|
+
if "intent_mapping" in state and len(state["intent_mapping"]["intents"]) > 0:
|
|
1105
|
+
intents = state["intent_mapping"]["intents"]
|
|
1106
|
+
intents_str = ""
|
|
1107
|
+
for intent in intents:
|
|
1108
|
+
intents_str += f"-Mapped intent: `{intent['intent'].intent_value}`, tool to call: `{intent['intent'].intent_target}`\n"
|
|
1109
|
+
|
|
1110
|
+
if "<intents_rules>" not in self._system_prompt:
|
|
1111
|
+
updated_system_prompt = f"""{self._system_prompt}
|
|
1112
|
+
|
|
1113
|
+
<intents_rules>
|
|
1114
|
+
Everytime a user is sending a message, a system is trying to map the prompt/message to an intent or a list of intents using a vector search.
|
|
1115
|
+
The following is the list of mapped intents. This list will change over time as new messages comes in.
|
|
1116
|
+
You must analyze if the user message and the mapped intents are related to each other.
|
|
1117
|
+
If it's the case, you must take them into account, otherwise you must ignore the ones that are not related.
|
|
1118
|
+
If you endup with a single intent which is of type RAW, you must output the intent_target and nothing else as there will be tests asserting the correctness of the output.
|
|
1119
|
+
If you endup with a single intent which is of type TOOL, you must call this tool.
|
|
1120
|
+
|
|
1121
|
+
<intents>\n{intents_str}\n</intents>
|
|
1122
|
+
|
|
1123
|
+
</intents_rules>
|
|
1124
|
+
"""
|
|
1125
|
+
else:
|
|
1126
|
+
import re
|
|
1127
|
+
|
|
1128
|
+
pattern = r"(<intents>)(.*?)(</intents>)"
|
|
1129
|
+
|
|
1130
|
+
def replace(match):
|
|
1131
|
+
return f"{match.group(1)}\n{intents_str}\n{match.group(3)}"
|
|
1132
|
+
|
|
1133
|
+
updated_system_prompt = re.sub(
|
|
1134
|
+
pattern, replace, self._system_prompt, flags=re.DOTALL
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
self._system_prompt = updated_system_prompt
|
|
1138
|
+
self.set_system_prompt(self._system_prompt)
|
|
1139
|
+
logger.debug(f"Injected in system prompt: {self._system_prompt}")
|
|
1140
|
+
|
|
1141
|
+
def duplicate(
|
|
1142
|
+
self,
|
|
1143
|
+
queue: Queue | None = None,
|
|
1144
|
+
agent_shared_state: AgentSharedState | None = None,
|
|
1145
|
+
) -> "IntentAgent":
|
|
1146
|
+
"""Create a new instance of the agent with the same configuration.
|
|
1147
|
+
|
|
1148
|
+
This method creates a deep copy of the agent with the same configuration
|
|
1149
|
+
but with its own independent state. This is useful when you need to run
|
|
1150
|
+
multiple instances of the same agent concurrently.
|
|
1151
|
+
|
|
1152
|
+
Returns:
|
|
1153
|
+
IntentAgent: A new IntentAgent instance with the same configuration
|
|
1154
|
+
"""
|
|
1155
|
+
shared_state = agent_shared_state or AgentSharedState()
|
|
1156
|
+
|
|
1157
|
+
if queue is None:
|
|
1158
|
+
queue = Queue()
|
|
1159
|
+
|
|
1160
|
+
# We duplicated each agent and add them as tools.
|
|
1161
|
+
# This will be recursively done for each sub agents.
|
|
1162
|
+
agents: list[Union["IntentAgent", "Agent"]] = [
|
|
1163
|
+
agent.duplicate(queue, shared_state) for agent in self._original_agents
|
|
1164
|
+
]
|
|
1165
|
+
|
|
1166
|
+
new_agent = IntentAgent(
|
|
1167
|
+
name=self._name,
|
|
1168
|
+
description=self._description,
|
|
1169
|
+
chat_model=self._chat_model,
|
|
1170
|
+
tools=self._original_tools,
|
|
1171
|
+
agents=agents,
|
|
1172
|
+
intents=self._intents,
|
|
1173
|
+
memory=self._checkpointer,
|
|
1174
|
+
state=shared_state, # Create new state instance
|
|
1175
|
+
configuration=self._configuration,
|
|
1176
|
+
event_queue=queue,
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
return new_agent
|