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.
Files changed (124) hide show
  1. assets/favicon.ico +0 -0
  2. assets/logo.png +0 -0
  3. naas_abi_core/__init__.py +1 -0
  4. naas_abi_core/apps/api/api.py +245 -0
  5. naas_abi_core/apps/api/api_test.py +281 -0
  6. naas_abi_core/apps/api/openapi_doc.py +144 -0
  7. naas_abi_core/apps/mcp/Dockerfile.mcp +35 -0
  8. naas_abi_core/apps/mcp/mcp_server.py +243 -0
  9. naas_abi_core/apps/mcp/mcp_server_test.py +163 -0
  10. naas_abi_core/apps/terminal_agent/main.py +555 -0
  11. naas_abi_core/apps/terminal_agent/terminal_style.py +175 -0
  12. naas_abi_core/engine/Engine.py +87 -0
  13. naas_abi_core/engine/EngineProxy.py +109 -0
  14. naas_abi_core/engine/Engine_test.py +6 -0
  15. naas_abi_core/engine/IEngine.py +91 -0
  16. naas_abi_core/engine/conftest.py +45 -0
  17. naas_abi_core/engine/engine_configuration/EngineConfiguration.py +216 -0
  18. naas_abi_core/engine/engine_configuration/EngineConfiguration_Deploy.py +7 -0
  19. naas_abi_core/engine/engine_configuration/EngineConfiguration_GenericLoader.py +49 -0
  20. naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService.py +159 -0
  21. naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService_test.py +26 -0
  22. naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService.py +138 -0
  23. naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService_test.py +74 -0
  24. naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService.py +224 -0
  25. naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService_test.py +109 -0
  26. naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService.py +76 -0
  27. naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService_test.py +33 -0
  28. naas_abi_core/engine/engine_configuration/EngineConfiguration_test.py +9 -0
  29. naas_abi_core/engine/engine_configuration/utils/PydanticModelValidator.py +15 -0
  30. naas_abi_core/engine/engine_loaders/EngineModuleLoader.py +302 -0
  31. naas_abi_core/engine/engine_loaders/EngineOntologyLoader.py +16 -0
  32. naas_abi_core/engine/engine_loaders/EngineServiceLoader.py +47 -0
  33. naas_abi_core/integration/__init__.py +7 -0
  34. naas_abi_core/integration/integration.py +28 -0
  35. naas_abi_core/models/Model.py +198 -0
  36. naas_abi_core/models/OpenRouter.py +18 -0
  37. naas_abi_core/models/OpenRouter_test.py +36 -0
  38. naas_abi_core/module/Module.py +252 -0
  39. naas_abi_core/module/ModuleAgentLoader.py +50 -0
  40. naas_abi_core/module/ModuleUtils.py +20 -0
  41. naas_abi_core/modules/templatablesparqlquery/README.md +196 -0
  42. naas_abi_core/modules/templatablesparqlquery/__init__.py +39 -0
  43. naas_abi_core/modules/templatablesparqlquery/ontologies/TemplatableSparqlQueryOntology.ttl +116 -0
  44. naas_abi_core/modules/templatablesparqlquery/workflows/GenericWorkflow.py +48 -0
  45. naas_abi_core/modules/templatablesparqlquery/workflows/TemplatableSparqlQueryLoader.py +192 -0
  46. naas_abi_core/pipeline/__init__.py +6 -0
  47. naas_abi_core/pipeline/pipeline.py +70 -0
  48. naas_abi_core/services/__init__.py +0 -0
  49. naas_abi_core/services/agent/Agent.py +1619 -0
  50. naas_abi_core/services/agent/AgentMemory_test.py +28 -0
  51. naas_abi_core/services/agent/Agent_test.py +214 -0
  52. naas_abi_core/services/agent/IntentAgent.py +1179 -0
  53. naas_abi_core/services/agent/IntentAgent_test.py +139 -0
  54. naas_abi_core/services/agent/beta/Embeddings.py +181 -0
  55. naas_abi_core/services/agent/beta/IntentMapper.py +120 -0
  56. naas_abi_core/services/agent/beta/LocalModel.py +88 -0
  57. naas_abi_core/services/agent/beta/VectorStore.py +89 -0
  58. naas_abi_core/services/agent/test_agent_memory.py +278 -0
  59. naas_abi_core/services/agent/test_postgres_integration.py +145 -0
  60. naas_abi_core/services/cache/CacheFactory.py +31 -0
  61. naas_abi_core/services/cache/CachePort.py +63 -0
  62. naas_abi_core/services/cache/CacheService.py +246 -0
  63. naas_abi_core/services/cache/CacheService_test.py +85 -0
  64. naas_abi_core/services/cache/adapters/secondary/CacheFSAdapter.py +39 -0
  65. naas_abi_core/services/object_storage/ObjectStorageFactory.py +57 -0
  66. naas_abi_core/services/object_storage/ObjectStoragePort.py +47 -0
  67. naas_abi_core/services/object_storage/ObjectStorageService.py +41 -0
  68. naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterFS.py +52 -0
  69. naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterNaas.py +131 -0
  70. naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterS3.py +171 -0
  71. naas_abi_core/services/ontology/OntologyPorts.py +36 -0
  72. naas_abi_core/services/ontology/OntologyService.py +17 -0
  73. naas_abi_core/services/ontology/adaptors/secondary/OntologyService_SecondaryAdaptor_NERPort.py +37 -0
  74. naas_abi_core/services/secret/Secret.py +138 -0
  75. naas_abi_core/services/secret/SecretPorts.py +45 -0
  76. naas_abi_core/services/secret/Secret_test.py +65 -0
  77. naas_abi_core/services/secret/adaptors/secondary/Base64Secret.py +57 -0
  78. naas_abi_core/services/secret/adaptors/secondary/Base64Secret_test.py +39 -0
  79. naas_abi_core/services/secret/adaptors/secondary/NaasSecret.py +88 -0
  80. naas_abi_core/services/secret/adaptors/secondary/NaasSecret_test.py +25 -0
  81. naas_abi_core/services/secret/adaptors/secondary/dotenv_secret_secondaryadaptor.py +29 -0
  82. naas_abi_core/services/triple_store/TripleStoreFactory.py +116 -0
  83. naas_abi_core/services/triple_store/TripleStorePorts.py +223 -0
  84. naas_abi_core/services/triple_store/TripleStoreService.py +419 -0
  85. naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune.py +1300 -0
  86. naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune_test.py +284 -0
  87. naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph.py +597 -0
  88. naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph_test.py +1474 -0
  89. naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__Filesystem.py +223 -0
  90. naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__ObjectStorage.py +234 -0
  91. naas_abi_core/services/triple_store/adaptors/secondary/base/TripleStoreService__SecondaryAdaptor__FileBase.py +18 -0
  92. naas_abi_core/services/vector_store/IVectorStorePort.py +101 -0
  93. naas_abi_core/services/vector_store/IVectorStorePort_test.py +189 -0
  94. naas_abi_core/services/vector_store/VectorStoreFactory.py +47 -0
  95. naas_abi_core/services/vector_store/VectorStoreService.py +171 -0
  96. naas_abi_core/services/vector_store/VectorStoreService_test.py +185 -0
  97. naas_abi_core/services/vector_store/__init__.py +13 -0
  98. naas_abi_core/services/vector_store/adapters/QdrantAdapter.py +251 -0
  99. naas_abi_core/services/vector_store/adapters/QdrantAdapter_test.py +57 -0
  100. naas_abi_core/tests/test_services_imports.py +69 -0
  101. naas_abi_core/utils/Expose.py +55 -0
  102. naas_abi_core/utils/Graph.py +182 -0
  103. naas_abi_core/utils/JSON.py +49 -0
  104. naas_abi_core/utils/LazyLoader.py +44 -0
  105. naas_abi_core/utils/Logger.py +12 -0
  106. naas_abi_core/utils/OntologyReasoner.py +141 -0
  107. naas_abi_core/utils/OntologyYaml.py +681 -0
  108. naas_abi_core/utils/SPARQL.py +256 -0
  109. naas_abi_core/utils/Storage.py +33 -0
  110. naas_abi_core/utils/StorageUtils.py +398 -0
  111. naas_abi_core/utils/String.py +52 -0
  112. naas_abi_core/utils/Workers.py +114 -0
  113. naas_abi_core/utils/__init__.py +0 -0
  114. naas_abi_core/utils/onto2py/README.md +0 -0
  115. naas_abi_core/utils/onto2py/__init__.py +10 -0
  116. naas_abi_core/utils/onto2py/__main__.py +29 -0
  117. naas_abi_core/utils/onto2py/onto2py.py +611 -0
  118. naas_abi_core/utils/onto2py/tests/ttl2py_test.py +271 -0
  119. naas_abi_core/workflow/__init__.py +5 -0
  120. naas_abi_core/workflow/workflow.py +48 -0
  121. naas_abi_core-1.4.1.dist-info/METADATA +630 -0
  122. naas_abi_core-1.4.1.dist-info/RECORD +124 -0
  123. naas_abi_core-1.4.1.dist-info/WHEEL +4 -0
  124. 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