fastworkflow 2.15.5__py3-none-any.whl → 2.17.13__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.
- fastworkflow/_workflows/command_metadata_extraction/_commands/ErrorCorrection/you_misunderstood.py +1 -1
- fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/what_can_i_do.py +16 -2
- fastworkflow/_workflows/command_metadata_extraction/_commands/wildcard.py +27 -570
- fastworkflow/_workflows/command_metadata_extraction/intent_detection.py +360 -0
- fastworkflow/_workflows/command_metadata_extraction/parameter_extraction.py +411 -0
- fastworkflow/chat_session.py +379 -206
- fastworkflow/cli.py +80 -165
- fastworkflow/command_context_model.py +73 -7
- fastworkflow/command_executor.py +14 -5
- fastworkflow/command_metadata_api.py +106 -6
- fastworkflow/examples/fastworkflow.env +2 -1
- fastworkflow/examples/fastworkflow.passwords.env +2 -1
- fastworkflow/examples/retail_workflow/_commands/exchange_delivered_order_items.py +32 -3
- fastworkflow/examples/retail_workflow/_commands/find_user_id_by_email.py +6 -5
- fastworkflow/examples/retail_workflow/_commands/modify_pending_order_items.py +32 -3
- fastworkflow/examples/retail_workflow/_commands/return_delivered_order_items.py +13 -2
- fastworkflow/examples/retail_workflow/_commands/transfer_to_human_agents.py +1 -1
- fastworkflow/intent_clarification_agent.py +131 -0
- fastworkflow/mcp_server.py +3 -3
- fastworkflow/run/__main__.py +33 -40
- fastworkflow/run_fastapi_mcp/README.md +373 -0
- fastworkflow/run_fastapi_mcp/__main__.py +1300 -0
- fastworkflow/run_fastapi_mcp/conversation_store.py +391 -0
- fastworkflow/run_fastapi_mcp/jwt_manager.py +341 -0
- fastworkflow/run_fastapi_mcp/mcp_specific.py +103 -0
- fastworkflow/run_fastapi_mcp/redoc_2_standalone_html.py +40 -0
- fastworkflow/run_fastapi_mcp/utils.py +517 -0
- fastworkflow/train/__main__.py +1 -1
- fastworkflow/utils/chat_adapter.py +99 -0
- fastworkflow/utils/python_utils.py +4 -4
- fastworkflow/utils/react.py +258 -0
- fastworkflow/utils/signatures.py +338 -139
- fastworkflow/workflow.py +1 -5
- fastworkflow/workflow_agent.py +185 -133
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/METADATA +16 -18
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/RECORD +40 -30
- fastworkflow/run_agent/__main__.py +0 -294
- fastworkflow/run_agent/agent_module.py +0 -194
- /fastworkflow/{run_agent → run_fastapi_mcp}/__init__.py +0 -0
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/LICENSE +0 -0
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/WHEEL +0 -0
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/entry_points.txt +0 -0
fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/what_can_i_do.py
CHANGED
|
@@ -8,6 +8,7 @@ import json
|
|
|
8
8
|
|
|
9
9
|
import fastworkflow
|
|
10
10
|
from fastworkflow.train.generate_synthetic import generate_diverse_utterances
|
|
11
|
+
from fastworkflow.command_context_model import get_workflow_info
|
|
11
12
|
from fastworkflow.command_metadata_api import CommandMetadataAPI
|
|
12
13
|
|
|
13
14
|
class Signature:
|
|
@@ -139,20 +140,33 @@ class ResponseGenerator:
|
|
|
139
140
|
with contextlib.suppress(Exception):
|
|
140
141
|
if fastworkflow.chat_session:
|
|
141
142
|
is_agent_mode = fastworkflow.chat_session.run_as_agent
|
|
142
|
-
|
|
143
|
+
|
|
143
144
|
app_workflow = workflow.context["app_workflow"]
|
|
144
|
-
|
|
145
|
+
|
|
146
|
+
# Get workflow definition
|
|
147
|
+
workflow_info = get_workflow_info(app_workflow.folderpath)
|
|
148
|
+
workflow_def_text = CommandMetadataAPI.get_workflow_definition_display_text(workflow_info)
|
|
149
|
+
|
|
150
|
+
# Get available commands in current context
|
|
151
|
+
commands_text = CommandMetadataAPI.get_command_display_text(
|
|
145
152
|
subject_workflow_path=app_workflow.folderpath,
|
|
146
153
|
cme_workflow_path=workflow.folderpath,
|
|
147
154
|
active_context_name=app_workflow.current_command_context_name,
|
|
148
155
|
for_agents=is_agent_mode,
|
|
149
156
|
)
|
|
157
|
+
|
|
158
|
+
response = f"{workflow_def_text}\n\n{commands_text}"
|
|
150
159
|
|
|
160
|
+
nlu_pipeline_stage = workflow.context.get(
|
|
161
|
+
"NLU_Pipeline_Stage",
|
|
162
|
+
fastworkflow.NLUPipelineStage.INTENT_DETECTION)
|
|
163
|
+
success = nlu_pipeline_stage == fastworkflow.NLUPipelineStage.INTENT_DETECTION
|
|
151
164
|
return fastworkflow.CommandOutput(
|
|
152
165
|
workflow_id=workflow.id,
|
|
153
166
|
command_responses=[
|
|
154
167
|
fastworkflow.CommandResponse(
|
|
155
168
|
response=response,
|
|
169
|
+
success=success
|
|
156
170
|
)
|
|
157
171
|
]
|
|
158
172
|
)
|
|
@@ -1,569 +1,9 @@
|
|
|
1
|
-
import contextlib
|
|
2
|
-
from enum import Enum
|
|
3
|
-
import sys
|
|
4
|
-
from typing import Dict, List, Optional, Type, Union
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
|
|
8
|
-
from pydantic import BaseModel
|
|
9
|
-
from pydantic_core import PydanticUndefined
|
|
10
|
-
from speedict import Rdict
|
|
11
|
-
|
|
12
1
|
import fastworkflow
|
|
13
|
-
from fastworkflow
|
|
14
|
-
from fastworkflow import Action, CommandOutput, CommandResponse, ModuleType, NLUPipelineStage
|
|
15
|
-
from fastworkflow.cache_matching import cache_match, store_utterance_cache
|
|
2
|
+
from fastworkflow import Action, CommandOutput, CommandResponse, NLUPipelineStage
|
|
16
3
|
from fastworkflow.command_executor import CommandExecutor
|
|
17
|
-
from fastworkflow.command_routing import RoutingDefinition
|
|
18
|
-
import fastworkflow.command_routing
|
|
19
|
-
from fastworkflow.model_pipeline_training import (
|
|
20
|
-
predict_single_sentence,
|
|
21
|
-
get_artifact_path,
|
|
22
|
-
CommandRouter
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
from fastworkflow.train.generate_synthetic import generate_diverse_utterances
|
|
26
|
-
from fastworkflow.utils.fuzzy_match import find_best_matches
|
|
27
|
-
from fastworkflow.utils.signatures import InputForParamExtraction
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
INVALID_INT_VALUE = -sys.maxsize
|
|
31
|
-
INVALID_FLOAT_VALUE = -sys.float_info.max
|
|
32
|
-
|
|
33
|
-
MISSING_INFORMATION_ERRMSG = fastworkflow.get_env_var("MISSING_INFORMATION_ERRMSG")
|
|
34
|
-
INVALID_INFORMATION_ERRMSG = fastworkflow.get_env_var("INVALID_INFORMATION_ERRMSG")
|
|
35
|
-
|
|
36
|
-
NOT_FOUND = fastworkflow.get_env_var("NOT_FOUND")
|
|
37
|
-
INVALID = fastworkflow.get_env_var("INVALID")
|
|
38
|
-
PARAMETER_EXTRACTION_ERROR_MSG = None
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class CommandNamePrediction:
|
|
42
|
-
class Output(BaseModel):
|
|
43
|
-
command_name: Optional[str] = None
|
|
44
|
-
error_msg: Optional[str] = None
|
|
45
|
-
is_cme_command: bool = False
|
|
46
|
-
|
|
47
|
-
def __init__(self, cme_workflow: fastworkflow.Workflow):
|
|
48
|
-
self.cme_workflow = cme_workflow
|
|
49
|
-
self.app_workflow = cme_workflow.context["app_workflow"]
|
|
50
|
-
self.app_workflow_folderpath = self.app_workflow.folderpath
|
|
51
|
-
self.app_workflow_id = self.app_workflow.id
|
|
52
|
-
|
|
53
|
-
self.convo_path = os.path.join(self.app_workflow_folderpath, "___convo_info")
|
|
54
|
-
self.cache_path = self._get_cache_path(self.app_workflow_id, self.convo_path)
|
|
55
|
-
self.path = self._get_cache_path_cache(self.convo_path)
|
|
56
|
-
|
|
57
|
-
def predict(self, command_context_name: str, command: str, nlu_pipeline_stage: NLUPipelineStage) -> "CommandNamePrediction.Output":
|
|
58
|
-
# sourcery skip: extract-duplicate-method
|
|
59
|
-
|
|
60
|
-
model_artifact_path = f"{self.app_workflow_folderpath}/___command_info/{command_context_name}"
|
|
61
|
-
command_router = CommandRouter(model_artifact_path)
|
|
62
|
-
|
|
63
|
-
# Re-use the already-built ModelPipeline attached to the router
|
|
64
|
-
# instead of instantiating a fresh one. This avoids reloading HF
|
|
65
|
-
# checkpoints and transferring tensors each time we see a new
|
|
66
|
-
# message for the same context.
|
|
67
|
-
modelpipeline = command_router.modelpipeline
|
|
68
|
-
|
|
69
|
-
crd = fastworkflow.RoutingRegistry.get_definition(
|
|
70
|
-
self.cme_workflow.folderpath)
|
|
71
|
-
cme_command_names = crd.get_command_names('IntentDetection')
|
|
72
|
-
|
|
73
|
-
valid_command_names = set()
|
|
74
|
-
if nlu_pipeline_stage == NLUPipelineStage.INTENT_AMBIGUITY_CLARIFICATION:
|
|
75
|
-
valid_command_names = self._get_suggested_commands(self.path)
|
|
76
|
-
elif nlu_pipeline_stage in (
|
|
77
|
-
NLUPipelineStage.INTENT_DETECTION, NLUPipelineStage.INTENT_MISUNDERSTANDING_CLARIFICATION):
|
|
78
|
-
app_crd = fastworkflow.RoutingRegistry.get_definition(
|
|
79
|
-
self.app_workflow_folderpath)
|
|
80
|
-
valid_command_names = (
|
|
81
|
-
set(cme_command_names) |
|
|
82
|
-
set(app_crd.get_command_names(command_context_name))
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
command_name_dict = {
|
|
86
|
-
fully_qualified_command_name.split('/')[-1]: fully_qualified_command_name
|
|
87
|
-
for fully_qualified_command_name in valid_command_names
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if nlu_pipeline_stage == NLUPipelineStage.INTENT_AMBIGUITY_CLARIFICATION:
|
|
91
|
-
# what_can_i_do is special in INTENT_AMBIGUITY_CLARIFICATION
|
|
92
|
-
# We will not predict, just match plain utterances with exact or fuzzy match
|
|
93
|
-
command_name_dict |= {
|
|
94
|
-
plain_utterance: 'IntentDetection/what_can_i_do'
|
|
95
|
-
for plain_utterance in crd.command_directory.map_command_2_utterance_metadata[
|
|
96
|
-
'IntentDetection/what_can_i_do'
|
|
97
|
-
].plain_utterances
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if nlu_pipeline_stage != NLUPipelineStage.INTENT_DETECTION:
|
|
101
|
-
# abort is special.
|
|
102
|
-
# We will not predict, just match plain utterances with exact or fuzzy match
|
|
103
|
-
command_name_dict |= {
|
|
104
|
-
plain_utterance: 'ErrorCorrection/abort'
|
|
105
|
-
for plain_utterance in crd.command_directory.map_command_2_utterance_metadata[
|
|
106
|
-
'ErrorCorrection/abort'
|
|
107
|
-
].plain_utterances
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if nlu_pipeline_stage != NLUPipelineStage.INTENT_MISUNDERSTANDING_CLARIFICATION:
|
|
111
|
-
# you_misunderstood is special.
|
|
112
|
-
# We will not predict, just match plain utterances with exact or fuzzy match
|
|
113
|
-
command_name_dict |= {
|
|
114
|
-
plain_utterance: 'ErrorCorrection/you_misunderstood'
|
|
115
|
-
for plain_utterance in crd.command_directory.map_command_2_utterance_metadata[
|
|
116
|
-
'ErrorCorrection/you_misunderstood'
|
|
117
|
-
].plain_utterances
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
# See if the command starts with a command name followed by a space
|
|
121
|
-
tentative_command_name = command.split(" ", 1)[0]
|
|
122
|
-
normalized_command_name = tentative_command_name.lower()
|
|
123
|
-
command_name = None
|
|
124
|
-
if normalized_command_name in command_name_dict:
|
|
125
|
-
command_name = normalized_command_name
|
|
126
|
-
command = command.replace(f"{tentative_command_name}", "").strip().replace(" ", " ")
|
|
127
|
-
else:
|
|
128
|
-
# Use Levenshtein distance for fuzzy matching with the full command part after @
|
|
129
|
-
best_matched_commands, _ = find_best_matches(
|
|
130
|
-
command.replace(" ", "_"),
|
|
131
|
-
command_name_dict.keys(),
|
|
132
|
-
threshold=0.3 # Adjust threshold as needed
|
|
133
|
-
)
|
|
134
|
-
if best_matched_commands:
|
|
135
|
-
command_name = best_matched_commands[0]
|
|
136
|
-
|
|
137
|
-
if nlu_pipeline_stage == NLUPipelineStage.INTENT_DETECTION:
|
|
138
|
-
if not command_name:
|
|
139
|
-
if cache_result := cache_match(self.path, command, modelpipeline, 0.85):
|
|
140
|
-
command_name = cache_result
|
|
141
|
-
else:
|
|
142
|
-
predictions=command_router.predict(command)
|
|
143
|
-
|
|
144
|
-
if len(predictions)==1:
|
|
145
|
-
command_name = predictions[0].split('/')[-1]
|
|
146
|
-
else:
|
|
147
|
-
# If confidence is low, treat as ambiguous command (type 1)
|
|
148
|
-
error_msg = self._formulate_ambiguous_command_error_message(predictions)
|
|
149
|
-
# Store suggested commands
|
|
150
|
-
self._store_suggested_commands(self.path, predictions, 1)
|
|
151
|
-
return CommandNamePrediction.Output(error_msg=error_msg)
|
|
152
|
-
|
|
153
|
-
elif nlu_pipeline_stage in (
|
|
154
|
-
NLUPipelineStage.INTENT_AMBIGUITY_CLARIFICATION,
|
|
155
|
-
NLUPipelineStage.INTENT_MISUNDERSTANDING_CLARIFICATION
|
|
156
|
-
) and not command_name:
|
|
157
|
-
command_name = "what_can_i_do"
|
|
158
|
-
|
|
159
|
-
if not command_name or command_name == "wildcard":
|
|
160
|
-
fully_qualified_command_name=None
|
|
161
|
-
is_cme_command=False
|
|
162
|
-
else:
|
|
163
|
-
fully_qualified_command_name = command_name_dict[command_name]
|
|
164
|
-
is_cme_command=(
|
|
165
|
-
fully_qualified_command_name in cme_command_names or
|
|
166
|
-
fully_qualified_command_name in crd.get_command_names('ErrorCorrection')
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
nlu_pipeline_stage
|
|
171
|
-
in (
|
|
172
|
-
NLUPipelineStage.INTENT_AMBIGUITY_CLARIFICATION,
|
|
173
|
-
NLUPipelineStage.INTENT_MISUNDERSTANDING_CLARIFICATION,
|
|
174
|
-
)
|
|
175
|
-
and not fully_qualified_command_name.endswith('abort')
|
|
176
|
-
and not fully_qualified_command_name.endswith('what_can_i_do')
|
|
177
|
-
and not fully_qualified_command_name.endswith('you_misunderstood')
|
|
178
|
-
):
|
|
179
|
-
command = self.cme_workflow.context["command"]
|
|
180
|
-
store_utterance_cache(self.path, command, command_name, modelpipeline)
|
|
181
|
-
|
|
182
|
-
return CommandNamePrediction.Output(
|
|
183
|
-
command_name=fully_qualified_command_name,
|
|
184
|
-
is_cme_command=is_cme_command
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
@staticmethod
|
|
188
|
-
def _get_cache_path(workflow_id, convo_path):
|
|
189
|
-
"""
|
|
190
|
-
Generate cache file path based on workflow ID
|
|
191
|
-
"""
|
|
192
|
-
base_dir = convo_path
|
|
193
|
-
# Create directory if it doesn't exist
|
|
194
|
-
os.makedirs(base_dir, exist_ok=True)
|
|
195
|
-
return os.path.join(base_dir, f"{workflow_id}.db")
|
|
196
|
-
|
|
197
|
-
@staticmethod
|
|
198
|
-
def _get_cache_path_cache(convo_path):
|
|
199
|
-
"""
|
|
200
|
-
Generate cache file path based on workflow ID
|
|
201
|
-
"""
|
|
202
|
-
base_dir = convo_path
|
|
203
|
-
# Create directory if it doesn't exist
|
|
204
|
-
os.makedirs(base_dir, exist_ok=True)
|
|
205
|
-
return os.path.join(base_dir, "cache.db")
|
|
206
|
-
|
|
207
|
-
# Store the suggested commands with the flag type
|
|
208
|
-
@staticmethod
|
|
209
|
-
def _store_suggested_commands(cache_path, command_list, flag_type):
|
|
210
|
-
"""
|
|
211
|
-
Store the list of suggested commands for the constrained selection
|
|
212
|
-
|
|
213
|
-
Args:
|
|
214
|
-
cache_path: Path to the cache database
|
|
215
|
-
command_list: List of suggested commands
|
|
216
|
-
flag_type: Type of constraint (1=ambiguous, 2=misclassified)
|
|
217
|
-
"""
|
|
218
|
-
db = Rdict(cache_path)
|
|
219
|
-
try:
|
|
220
|
-
db["suggested_commands"] = command_list
|
|
221
|
-
db["flag_type"] = flag_type
|
|
222
|
-
finally:
|
|
223
|
-
db.close()
|
|
224
|
-
|
|
225
|
-
# Get the suggested commands
|
|
226
|
-
@staticmethod
|
|
227
|
-
def _get_suggested_commands(cache_path):
|
|
228
|
-
"""
|
|
229
|
-
Get the list of suggested commands for the constrained selection
|
|
230
|
-
"""
|
|
231
|
-
db = Rdict(cache_path)
|
|
232
|
-
try:
|
|
233
|
-
return db.get("suggested_commands", [])
|
|
234
|
-
finally:
|
|
235
|
-
db.close()
|
|
236
|
-
|
|
237
|
-
@staticmethod
|
|
238
|
-
def _get_count(cache_path):
|
|
239
|
-
db = Rdict(cache_path)
|
|
240
|
-
try:
|
|
241
|
-
return db.get("utterance_count", 0) # Default to 0 if key doesn't exist
|
|
242
|
-
finally:
|
|
243
|
-
db.close()
|
|
244
|
-
|
|
245
|
-
@staticmethod
|
|
246
|
-
def _print_db_contents(cache_path):
|
|
247
|
-
db = Rdict(cache_path)
|
|
248
|
-
try:
|
|
249
|
-
print("All keys in database:", list(db.keys()))
|
|
250
|
-
for key in db.keys():
|
|
251
|
-
print(f"Key: {key}, Value: {db[key]}")
|
|
252
|
-
finally:
|
|
253
|
-
db.close()
|
|
254
|
-
|
|
255
|
-
@staticmethod
|
|
256
|
-
def _store_utterance(cache_path, utterance, label):
|
|
257
|
-
"""
|
|
258
|
-
Store utterance in existing or new database
|
|
259
|
-
Returns: The utterance count used
|
|
260
|
-
"""
|
|
261
|
-
# Open the database (creates if doesn't exist)
|
|
262
|
-
db = Rdict(cache_path)
|
|
263
|
-
|
|
264
|
-
try:
|
|
265
|
-
# Get existing counter or initialize to 0
|
|
266
|
-
utterance_count = db.get("utterance_count", 0)
|
|
267
|
-
|
|
268
|
-
# Create and store the utterance entry
|
|
269
|
-
utterance_data = {
|
|
270
|
-
"utterance": utterance,
|
|
271
|
-
"label": label
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
db[utterance_count] = utterance_data
|
|
275
|
-
|
|
276
|
-
# Increment and store the counter
|
|
277
|
-
utterance_count += 1
|
|
278
|
-
db["utterance_count"] = utterance_count
|
|
279
|
-
|
|
280
|
-
return utterance_count - 1 # Return the count used for this utterance
|
|
281
|
-
|
|
282
|
-
finally:
|
|
283
|
-
# Always close the database
|
|
284
|
-
db.close()
|
|
285
|
-
|
|
286
|
-
# Function to read from database
|
|
287
|
-
@staticmethod
|
|
288
|
-
def _read_utterance(cache_path, utterance_id):
|
|
289
|
-
"""
|
|
290
|
-
Read a specific utterance from the database
|
|
291
|
-
"""
|
|
292
|
-
db = Rdict(cache_path)
|
|
293
|
-
try:
|
|
294
|
-
return db.get(utterance_id)['utterance']
|
|
295
|
-
finally:
|
|
296
|
-
db.close()
|
|
297
|
-
|
|
298
|
-
@staticmethod
|
|
299
|
-
def _formulate_ambiguous_command_error_message(route_choice_list: list[str]) -> str:
|
|
300
|
-
command_list = (
|
|
301
|
-
"\n".join([
|
|
302
|
-
f"{route_choice.split('/')[-1].lower()}"
|
|
303
|
-
for route_choice in route_choice_list if route_choice != 'wildcard'
|
|
304
|
-
])
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
return (
|
|
308
|
-
"The command is ambiguous. Please select from these possible options:\n"
|
|
309
|
-
f"{command_list}\n\n"
|
|
310
|
-
"or type 'what can i do' to see all commands\n"
|
|
311
|
-
"or type 'abort' to cancel"
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
class ParameterExtraction:
|
|
315
|
-
class Output(BaseModel):
|
|
316
|
-
parameters_are_valid: bool
|
|
317
|
-
cmd_parameters: Optional[BaseModel] = None
|
|
318
|
-
error_msg: Optional[str] = None
|
|
319
|
-
suggestions: Optional[Dict[str, List[str]]] = None
|
|
320
|
-
|
|
321
|
-
def __init__(self, cme_workflow: fastworkflow.Workflow, app_workflow: fastworkflow.Workflow, command_name: str, command: str):
|
|
322
|
-
self.cme_workflow = cme_workflow
|
|
323
|
-
self.app_workflow = app_workflow
|
|
324
|
-
self.command_name = command_name
|
|
325
|
-
self.command = command
|
|
326
|
-
|
|
327
|
-
def extract(self) -> "ParameterExtraction.Output":
|
|
328
|
-
app_workflow_folderpath = self.app_workflow.folderpath
|
|
329
|
-
app_command_routing_definition = fastworkflow.RoutingRegistry.get_definition(app_workflow_folderpath)
|
|
330
|
-
|
|
331
|
-
command_parameters_class = (
|
|
332
|
-
app_command_routing_definition.get_command_class(
|
|
333
|
-
self.command_name, ModuleType.COMMAND_PARAMETERS_CLASS
|
|
334
|
-
)
|
|
335
|
-
)
|
|
336
|
-
if not command_parameters_class:
|
|
337
|
-
return self.Output(parameters_are_valid=True)
|
|
338
4
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
self.command = self.command.replace(self.command_name, "").strip()
|
|
342
|
-
|
|
343
|
-
input_for_param_extraction = InputForParamExtraction.create(
|
|
344
|
-
self.app_workflow, self.command_name,
|
|
345
|
-
self.command)
|
|
346
|
-
|
|
347
|
-
if stored_params:
|
|
348
|
-
_, _, _, stored_missing_fields = self._extract_missing_fields(input_for_param_extraction, self.app_workflow, self.command_name, stored_params)
|
|
349
|
-
else:
|
|
350
|
-
stored_missing_fields = []
|
|
351
|
-
|
|
352
|
-
# If we have missing fields (in parameter extraction error state), try to apply the command directly
|
|
353
|
-
if stored_missing_fields:
|
|
354
|
-
# Apply the command directly as parameter values
|
|
355
|
-
direct_params = self._apply_missing_fields(self.command, stored_params, stored_missing_fields)
|
|
356
|
-
new_params = direct_params
|
|
357
|
-
else:
|
|
358
|
-
# Otherwise use the LLM-based extraction
|
|
359
|
-
new_params = input_for_param_extraction.extract_parameters(
|
|
360
|
-
command_parameters_class,
|
|
361
|
-
self.command_name,
|
|
362
|
-
app_workflow_folderpath)
|
|
363
|
-
|
|
364
|
-
if stored_params:
|
|
365
|
-
merged_params = self._merge_parameters(stored_params, new_params, stored_missing_fields)
|
|
366
|
-
else:
|
|
367
|
-
merged_params = new_params
|
|
368
|
-
|
|
369
|
-
self._store_parameters(self.cme_workflow, merged_params)
|
|
370
|
-
|
|
371
|
-
is_valid, error_msg, suggestions = input_for_param_extraction.validate_parameters(
|
|
372
|
-
self.app_workflow, self.command_name, merged_params
|
|
373
|
-
)
|
|
374
|
-
|
|
375
|
-
if not is_valid:
|
|
376
|
-
if params_str := self._format_parameters_for_display(merged_params):
|
|
377
|
-
error_msg = f"Extracted parameters so far:\n{params_str}\n\n{error_msg}"
|
|
378
|
-
|
|
379
|
-
error_msg += "\nEnter 'abort' to get out of this error state and/or execute a different command."
|
|
380
|
-
error_msg += "\nEnter 'you misunderstood' if the wrong command was executed."
|
|
381
|
-
return self.Output(
|
|
382
|
-
parameters_are_valid=False,
|
|
383
|
-
error_msg=error_msg,
|
|
384
|
-
cmd_parameters=merged_params,
|
|
385
|
-
suggestions=suggestions)
|
|
386
|
-
|
|
387
|
-
self._clear_parameters(self.cme_workflow)
|
|
388
|
-
return self.Output(
|
|
389
|
-
parameters_are_valid=True,
|
|
390
|
-
cmd_parameters=merged_params)
|
|
391
|
-
|
|
392
|
-
@staticmethod
|
|
393
|
-
def _get_stored_parameters(cme_workflow: fastworkflow.Workflow):
|
|
394
|
-
return cme_workflow.context.get("stored_parameters")
|
|
395
|
-
|
|
396
|
-
@staticmethod
|
|
397
|
-
def _store_parameters(cme_workflow: fastworkflow.Workflow, parameters):
|
|
398
|
-
cme_workflow.context["stored_parameters"] = parameters
|
|
399
|
-
|
|
400
|
-
@staticmethod
|
|
401
|
-
def _clear_parameters(cme_workflow: fastworkflow.Workflow):
|
|
402
|
-
if "stored_parameters" in cme_workflow.context:
|
|
403
|
-
del cme_workflow.context["stored_parameters"]
|
|
404
|
-
|
|
405
|
-
@staticmethod
|
|
406
|
-
def _extract_missing_fields(input_for_param_extraction, sws, command_name, stored_params):
|
|
407
|
-
stored_missing_fields = []
|
|
408
|
-
is_valid, error_msg, suggestions = input_for_param_extraction.validate_parameters(
|
|
409
|
-
sws, command_name, stored_params
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
if not is_valid:
|
|
413
|
-
if MISSING_INFORMATION_ERRMSG in error_msg:
|
|
414
|
-
missing_fields_str = error_msg.split(f"{MISSING_INFORMATION_ERRMSG}")[1].split("\n")[0]
|
|
415
|
-
stored_missing_fields = [f.strip() for f in missing_fields_str.split(",")]
|
|
416
|
-
if INVALID_INFORMATION_ERRMSG in error_msg:
|
|
417
|
-
invalid_section = error_msg.split(f"{INVALID_INFORMATION_ERRMSG}")[1]
|
|
418
|
-
if "\n" in invalid_section:
|
|
419
|
-
invalid_fields_str = invalid_section.split("\n")[0]
|
|
420
|
-
stored_missing_fields.extend(
|
|
421
|
-
invalid_field.split(" '")[0].strip()
|
|
422
|
-
for invalid_field in invalid_fields_str.split(", ")
|
|
423
|
-
)
|
|
424
|
-
return is_valid, error_msg, suggestions, stored_missing_fields
|
|
425
|
-
|
|
426
|
-
@staticmethod
|
|
427
|
-
def _merge_parameters(old_params, new_params, missing_fields):
|
|
428
|
-
"""
|
|
429
|
-
Merge new parameters with old parameters, prioritizing new values when appropriate.
|
|
430
|
-
"""
|
|
431
|
-
global PARAMETER_EXTRACTION_ERROR_MSG
|
|
432
|
-
if not PARAMETER_EXTRACTION_ERROR_MSG:
|
|
433
|
-
PARAMETER_EXTRACTION_ERROR_MSG = fastworkflow.get_env_var("PARAMETER_EXTRACTION_ERROR_MSG")
|
|
434
|
-
|
|
435
|
-
merged = old_params.model_copy()
|
|
436
|
-
|
|
437
|
-
try:
|
|
438
|
-
all_fields = list(old_params.model_fields.keys())
|
|
439
|
-
missing_fields = missing_fields or []
|
|
440
|
-
|
|
441
|
-
for field_name in all_fields:
|
|
442
|
-
if hasattr(new_params, field_name):
|
|
443
|
-
new_value = getattr(new_params, field_name)
|
|
444
|
-
old_value = getattr(merged, field_name)
|
|
445
|
-
|
|
446
|
-
if new_value is not None and new_value != NOT_FOUND:
|
|
447
|
-
if isinstance(old_value, str) and INVALID in old_value and INVALID not in new_value:
|
|
448
|
-
setattr(merged, field_name, new_value)
|
|
449
|
-
|
|
450
|
-
elif old_value is None or old_value == NOT_FOUND:
|
|
451
|
-
setattr(merged, field_name, new_value)
|
|
452
|
-
|
|
453
|
-
elif isinstance(old_value, int) and old_value == INVALID_INT_VALUE:
|
|
454
|
-
with contextlib.suppress(ValueError, TypeError):
|
|
455
|
-
setattr(merged, field_name, int(new_value))
|
|
456
|
-
|
|
457
|
-
elif isinstance(old_value, float) and old_value == INVALID_FLOAT_VALUE:
|
|
458
|
-
with contextlib.suppress(ValueError, TypeError):
|
|
459
|
-
setattr(merged, field_name, float(new_value))
|
|
460
|
-
|
|
461
|
-
elif (field_name in missing_fields and
|
|
462
|
-
hasattr(merged.model_fields.get(field_name), "json_schema_extra") and
|
|
463
|
-
merged.model_fields.get(field_name).json_schema_extra and
|
|
464
|
-
"db_lookup" in merged.model_fields.get(field_name).json_schema_extra):
|
|
465
|
-
setattr(merged, field_name, new_value)
|
|
466
|
-
|
|
467
|
-
elif field_name in missing_fields:
|
|
468
|
-
field_info = merged.model_fields.get(field_name)
|
|
469
|
-
has_pattern = hasattr(field_info, "pattern") and field_info.pattern is not None
|
|
470
|
-
|
|
471
|
-
if not has_pattern:
|
|
472
|
-
for meta in getattr(field_info, "metadata", []):
|
|
473
|
-
if hasattr(meta, "pattern"):
|
|
474
|
-
has_pattern = True
|
|
475
|
-
break
|
|
476
|
-
|
|
477
|
-
if not has_pattern and hasattr(field_info, "json_schema_extra") and field_info.json_schema_extra:
|
|
478
|
-
has_pattern = "pattern" in field_info.json_schema_extra
|
|
479
|
-
|
|
480
|
-
if has_pattern:
|
|
481
|
-
setattr(merged, field_name, new_value)
|
|
482
|
-
except Exception as exc:
|
|
483
|
-
logger.warning(PARAMETER_EXTRACTION_ERROR_MSG.format(error=exc))
|
|
484
|
-
|
|
485
|
-
return merged
|
|
486
|
-
|
|
487
|
-
@staticmethod
|
|
488
|
-
def _format_parameters_for_display(params):
|
|
489
|
-
"""
|
|
490
|
-
Format parameters for display in the error message.
|
|
491
|
-
"""
|
|
492
|
-
if not params:
|
|
493
|
-
return ""
|
|
494
|
-
|
|
495
|
-
lines = []
|
|
496
|
-
|
|
497
|
-
all_fields = list(params.model_fields.keys())
|
|
498
|
-
|
|
499
|
-
for field_name in all_fields:
|
|
500
|
-
value = getattr(params, field_name, None)
|
|
501
|
-
|
|
502
|
-
if value in [
|
|
503
|
-
NOT_FOUND,
|
|
504
|
-
None,
|
|
505
|
-
INVALID_INT_VALUE,
|
|
506
|
-
INVALID_FLOAT_VALUE
|
|
507
|
-
]:
|
|
508
|
-
continue
|
|
509
|
-
|
|
510
|
-
display_name = " ".join(word.capitalize() for word in field_name.split('_'))
|
|
511
|
-
|
|
512
|
-
# Format fields appropriately based on type
|
|
513
|
-
if (
|
|
514
|
-
isinstance(value, bool)
|
|
515
|
-
or not hasattr(value, 'value')
|
|
516
|
-
and isinstance(value, (int, float))
|
|
517
|
-
or not hasattr(value, 'value')
|
|
518
|
-
and isinstance(value, str)
|
|
519
|
-
or not hasattr(value, 'value')
|
|
520
|
-
):
|
|
521
|
-
lines.append(f"{display_name}: {value}")
|
|
522
|
-
else: # Handle enum types
|
|
523
|
-
lines.append(f"{display_name}: {value.value}")
|
|
524
|
-
return "\n".join(lines)
|
|
525
|
-
|
|
526
|
-
@staticmethod
|
|
527
|
-
def _apply_missing_fields(command: str, default_params: BaseModel, missing_fields: list):
|
|
528
|
-
global PARAMETER_EXTRACTION_ERROR_MSG
|
|
529
|
-
if not PARAMETER_EXTRACTION_ERROR_MSG:
|
|
530
|
-
PARAMETER_EXTRACTION_ERROR_MSG = fastworkflow.get_env_var("PARAMETER_EXTRACTION_ERROR_MSG")
|
|
531
|
-
|
|
532
|
-
params = default_params.model_copy()
|
|
533
|
-
|
|
534
|
-
try:
|
|
535
|
-
if "," in command:
|
|
536
|
-
parts = [part.strip() for part in command.split(",")]
|
|
537
|
-
|
|
538
|
-
if len(parts) == len(missing_fields):
|
|
539
|
-
if len(missing_fields) == 1:
|
|
540
|
-
field = missing_fields[0]
|
|
541
|
-
if hasattr(params, field):
|
|
542
|
-
setattr(params, field, parts[0])
|
|
543
|
-
return params
|
|
544
|
-
elif len(missing_fields) > 1:
|
|
545
|
-
for i, field in enumerate(missing_fields):
|
|
546
|
-
if i < len(parts) and hasattr(params, field):
|
|
547
|
-
setattr(params, field, parts[i])
|
|
548
|
-
return params
|
|
549
|
-
else:
|
|
550
|
-
if parts and missing_fields:
|
|
551
|
-
field = missing_fields[0]
|
|
552
|
-
if hasattr(params, field):
|
|
553
|
-
setattr(params, field, parts[0])
|
|
554
|
-
return params
|
|
555
|
-
|
|
556
|
-
elif missing_fields:
|
|
557
|
-
field = missing_fields[0]
|
|
558
|
-
if hasattr(params, field):
|
|
559
|
-
setattr(params, field, command.strip())
|
|
560
|
-
return params
|
|
561
|
-
|
|
562
|
-
except Exception as exc:
|
|
563
|
-
# logger.warning(PARAMETER_EXTRACTION_ERROR_MSG.format(error=exc))
|
|
564
|
-
pass
|
|
565
|
-
|
|
566
|
-
return params
|
|
5
|
+
from ..intent_detection import CommandNamePrediction
|
|
6
|
+
from ..parameter_extraction import ParameterExtraction
|
|
567
7
|
|
|
568
8
|
|
|
569
9
|
class Signature:
|
|
@@ -626,7 +66,11 @@ class ResponseGenerator:
|
|
|
626
66
|
workflow_context = workflow.context
|
|
627
67
|
if cnp_output.command_name == 'ErrorCorrection/you_misunderstood':
|
|
628
68
|
workflow_context["NLU_Pipeline_Stage"] = NLUPipelineStage.INTENT_MISUNDERSTANDING_CLARIFICATION
|
|
629
|
-
|
|
69
|
+
workflow_context["command"] = command
|
|
70
|
+
elif (
|
|
71
|
+
nlu_pipeline_stage == fastworkflow.NLUPipelineStage.INTENT_DETECTION or
|
|
72
|
+
cnp_output.command_name == 'ErrorCorrection/abort'
|
|
73
|
+
):
|
|
630
74
|
workflow.end_command_processing()
|
|
631
75
|
workflow.context = workflow_context
|
|
632
76
|
|
|
@@ -635,9 +79,13 @@ class ResponseGenerator:
|
|
|
635
79
|
command=command,
|
|
636
80
|
)
|
|
637
81
|
command_output = CommandExecutor.perform_action(workflow, startup_action)
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
82
|
+
if (
|
|
83
|
+
nlu_pipeline_stage == fastworkflow.NLUPipelineStage.INTENT_DETECTION or
|
|
84
|
+
cnp_output.command_name == 'ErrorCorrection/abort'
|
|
85
|
+
):
|
|
86
|
+
command_output.command_responses[0].artifacts["command_handled"] = True
|
|
87
|
+
# Set the additional attributes
|
|
88
|
+
command_output.command_name = cnp_output.command_name
|
|
641
89
|
return command_output
|
|
642
90
|
|
|
643
91
|
if nlu_pipeline_stage in {
|
|
@@ -664,6 +112,7 @@ class ResponseGenerator:
|
|
|
664
112
|
workflow_context = workflow.context
|
|
665
113
|
workflow_context["NLU_Pipeline_Stage"] = \
|
|
666
114
|
NLUPipelineStage.INTENT_MISUNDERSTANDING_CLARIFICATION
|
|
115
|
+
workflow_context["command"] = command
|
|
667
116
|
workflow.context = workflow_context
|
|
668
117
|
|
|
669
118
|
startup_action = Action(
|
|
@@ -686,11 +135,19 @@ class ResponseGenerator:
|
|
|
686
135
|
# move to the parameter extraction stage
|
|
687
136
|
workflow_context = workflow.context
|
|
688
137
|
workflow_context["NLU_Pipeline_Stage"] = NLUPipelineStage.PARAMETER_EXTRACTION
|
|
138
|
+
workflow.context = workflow_context
|
|
139
|
+
|
|
140
|
+
if nlu_pipeline_stage == NLUPipelineStage.PARAMETER_EXTRACTION:
|
|
141
|
+
cnp_output.command_name = workflow.context["command_name"]
|
|
142
|
+
else:
|
|
143
|
+
workflow_context = workflow.context
|
|
689
144
|
workflow_context["command_name"] = cnp_output.command_name
|
|
690
145
|
workflow.context = workflow_context
|
|
691
146
|
|
|
692
|
-
command_name =
|
|
693
|
-
|
|
147
|
+
command_name = cnp_output.command_name
|
|
148
|
+
# Use the preserved original command (with parameters) if available
|
|
149
|
+
preserved_command = f'{command_name}: {workflow.context.get("command", command)}'
|
|
150
|
+
extractor = ParameterExtraction(workflow, app_workflow, command_name, preserved_command)
|
|
694
151
|
pe_output = extractor.extract()
|
|
695
152
|
if not pe_output.parameters_are_valid:
|
|
696
153
|
return CommandOutput(
|
|
@@ -713,7 +170,7 @@ class ResponseGenerator:
|
|
|
713
170
|
CommandResponse(
|
|
714
171
|
response="",
|
|
715
172
|
artifacts={
|
|
716
|
-
"command":
|
|
173
|
+
"command": preserved_command,
|
|
717
174
|
"command_name": command_name,
|
|
718
175
|
"cmd_parameters": pe_output.cmd_parameters,
|
|
719
176
|
},
|