fastworkflow 2.17.8__py3-none-any.whl → 2.17.10__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/parameter_extraction.py +81 -5
- fastworkflow/chat_session.py +102 -33
- fastworkflow/command_metadata_api.py +50 -0
- fastworkflow/intent_clarification_agent.py +132 -0
- fastworkflow/run_fastapi_mcp/README.md +58 -40
- fastworkflow/run_fastapi_mcp/__main__.py +197 -114
- fastworkflow/run_fastapi_mcp/conversation_store.py +4 -4
- fastworkflow/run_fastapi_mcp/jwt_manager.py +24 -14
- fastworkflow/run_fastapi_mcp/utils.py +116 -52
- fastworkflow/train/__main__.py +1 -1
- fastworkflow/utils/react.py +13 -1
- fastworkflow/utils/signatures.py +49 -31
- fastworkflow/workflow_agent.py +78 -4
- {fastworkflow-2.17.8.dist-info → fastworkflow-2.17.10.dist-info}/METADATA +1 -1
- {fastworkflow-2.17.8.dist-info → fastworkflow-2.17.10.dist-info}/RECORD +18 -17
- {fastworkflow-2.17.8.dist-info → fastworkflow-2.17.10.dist-info}/LICENSE +0 -0
- {fastworkflow-2.17.8.dist-info → fastworkflow-2.17.10.dist-info}/WHEEL +0 -0
- {fastworkflow-2.17.8.dist-info → fastworkflow-2.17.10.dist-info}/entry_points.txt +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import sys
|
|
3
|
+
import re
|
|
3
4
|
from typing import Dict, List, Optional
|
|
4
5
|
|
|
5
6
|
from pydantic import BaseModel
|
|
7
|
+
from pydantic_core import PydanticUndefined
|
|
6
8
|
|
|
7
9
|
import fastworkflow
|
|
8
10
|
from fastworkflow.utils.logging import logger
|
|
@@ -59,11 +61,29 @@ class ParameterExtraction:
|
|
|
59
61
|
if stored_params:
|
|
60
62
|
new_params = self._extract_and_merge_missing_parameters(stored_params, self.command)
|
|
61
63
|
else:
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
self.
|
|
66
|
-
|
|
64
|
+
# Check if we're in agentic mode (not assistant mode command)
|
|
65
|
+
is_agentic_mode = (
|
|
66
|
+
"is_assistant_mode_command" not in self.cme_workflow.context
|
|
67
|
+
and "run_as_agent" in self.app_workflow.context
|
|
68
|
+
and self.app_workflow.context["run_as_agent"]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if is_agentic_mode:
|
|
72
|
+
# Try regex-based extraction first in agentic mode
|
|
73
|
+
new_params = self._extract_parameters_from_xml(self.command, command_parameters_class)
|
|
74
|
+
|
|
75
|
+
# If regex extraction fails, fall back to LLM-based extraction
|
|
76
|
+
if new_params is None:
|
|
77
|
+
new_params = input_for_param_extraction.extract_parameters(
|
|
78
|
+
command_parameters_class,
|
|
79
|
+
self.command_name,
|
|
80
|
+
app_workflow_folderpath)
|
|
81
|
+
else:
|
|
82
|
+
# Use LLM-based extraction for assistant mode
|
|
83
|
+
new_params = input_for_param_extraction.extract_parameters(
|
|
84
|
+
command_parameters_class,
|
|
85
|
+
self.command_name,
|
|
86
|
+
app_workflow_folderpath)
|
|
67
87
|
|
|
68
88
|
is_valid, error_msg, suggestions, missing_invalid_fields = \
|
|
69
89
|
input_for_param_extraction.validate_parameters(
|
|
@@ -272,6 +292,62 @@ class ParameterExtraction:
|
|
|
272
292
|
# Construct model without validation
|
|
273
293
|
return default_params.__class__.model_construct(**params_data)
|
|
274
294
|
|
|
295
|
+
@staticmethod
|
|
296
|
+
def _extract_parameters_from_xml(command: str, command_parameters_class: type[BaseModel]) -> Optional[BaseModel]:
|
|
297
|
+
"""
|
|
298
|
+
Extract parameters from XML-formatted command using regex.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
BaseModel instance with extracted parameters, or None if parsing fails
|
|
302
|
+
"""
|
|
303
|
+
field_names = list(command_parameters_class.model_fields.keys())
|
|
304
|
+
|
|
305
|
+
# If no parameters are defined, return empty model immediately
|
|
306
|
+
if not field_names:
|
|
307
|
+
return command_parameters_class.model_construct()
|
|
308
|
+
|
|
309
|
+
extracted_data = {}
|
|
310
|
+
|
|
311
|
+
# Try to extract each parameter using XML tags
|
|
312
|
+
for field_name in field_names:
|
|
313
|
+
# Look for <field_name>value</field_name> pattern
|
|
314
|
+
pattern = rf'<{re.escape(field_name)}>(.+?)</{re.escape(field_name)}>'
|
|
315
|
+
if match := re.search(pattern, command, re.DOTALL):
|
|
316
|
+
parameter_value = match[1].strip()
|
|
317
|
+
extracted_data[field_name] = parameter_value
|
|
318
|
+
|
|
319
|
+
# Check if we extracted values for ALL fields (safest criteria for LLM fallback)
|
|
320
|
+
all_fields_extracted = len(extracted_data) == len(field_names)
|
|
321
|
+
|
|
322
|
+
# Check if agent used example values
|
|
323
|
+
if all_fields_extracted:
|
|
324
|
+
for field_name, extracted_value in extracted_data.items():
|
|
325
|
+
field_info = command_parameters_class.model_fields[field_name]
|
|
326
|
+
examples = getattr(field_info, "examples", None)
|
|
327
|
+
if examples and extracted_value in examples:
|
|
328
|
+
all_fields_extracted = False
|
|
329
|
+
break
|
|
330
|
+
|
|
331
|
+
if all_fields_extracted:
|
|
332
|
+
# Initialize all fields with their default values (if they exist) or None
|
|
333
|
+
params_data = {}
|
|
334
|
+
for field_name in field_names:
|
|
335
|
+
field_info = command_parameters_class.model_fields[field_name]
|
|
336
|
+
if field_info.default is not PydanticUndefined:
|
|
337
|
+
params_data[field_name] = field_info.default
|
|
338
|
+
elif field_info.default_factory is not None:
|
|
339
|
+
params_data[field_name] = field_info.default_factory()
|
|
340
|
+
else:
|
|
341
|
+
params_data[field_name] = None
|
|
342
|
+
|
|
343
|
+
# Update with extracted values
|
|
344
|
+
params_data |= extracted_data
|
|
345
|
+
|
|
346
|
+
# Construct model without validation
|
|
347
|
+
return command_parameters_class.model_construct(**params_data)
|
|
348
|
+
|
|
349
|
+
return None
|
|
350
|
+
|
|
275
351
|
@staticmethod
|
|
276
352
|
def _extract_and_merge_missing_parameters(stored_params: BaseModel, command: str):
|
|
277
353
|
"""
|
fastworkflow/chat_session.py
CHANGED
|
@@ -8,6 +8,7 @@ import contextlib
|
|
|
8
8
|
import uuid
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
import os
|
|
11
|
+
import time
|
|
11
12
|
from datetime import datetime
|
|
12
13
|
|
|
13
14
|
import dspy
|
|
@@ -133,7 +134,8 @@ class ChatSession:
|
|
|
133
134
|
|
|
134
135
|
# Initialize agent-related attributes
|
|
135
136
|
self._run_as_agent = run_as_agent
|
|
136
|
-
self._workflow_tool_agent = None
|
|
137
|
+
self._workflow_tool_agent = None
|
|
138
|
+
self._intent_clarification_agent = None
|
|
137
139
|
|
|
138
140
|
# Create the command metadata extraction workflow with a unique ID
|
|
139
141
|
self._cme_workflow = fastworkflow.Workflow.create(
|
|
@@ -272,13 +274,23 @@ class ChatSession:
|
|
|
272
274
|
self._current_workflow.context["run_as_agent"] = True
|
|
273
275
|
|
|
274
276
|
# Initialize the workflow tool agent
|
|
275
|
-
from fastworkflow.workflow_agent import initialize_workflow_tool_agent
|
|
277
|
+
from fastworkflow.workflow_agent import initialize_workflow_tool_agent
|
|
276
278
|
self._workflow_tool_agent = initialize_workflow_tool_agent(self)
|
|
277
279
|
|
|
280
|
+
# Initialize the intent clarification agent
|
|
281
|
+
from fastworkflow.intent_clarification_agent import initialize_intent_clarification_agent
|
|
282
|
+
self._intent_clarification_agent = initialize_intent_clarification_agent(self)
|
|
283
|
+
|
|
278
284
|
@property
|
|
279
285
|
def workflow_tool_agent(self):
|
|
280
286
|
"""Get the workflow tool agent for agent mode."""
|
|
281
287
|
return self._workflow_tool_agent
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def intent_clarification_agent(self):
|
|
291
|
+
"""Get the intent clarification agent for agent mode."""
|
|
292
|
+
return self._intent_clarification_agent
|
|
293
|
+
|
|
282
294
|
@property
|
|
283
295
|
def cme_workflow(self) -> fastworkflow.Workflow:
|
|
284
296
|
"""Get the command metadata extraction workflow."""
|
|
@@ -360,21 +372,23 @@ class ChatSession:
|
|
|
360
372
|
try:
|
|
361
373
|
message = self.user_message_queue.get()
|
|
362
374
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
message.startswith('/')
|
|
367
|
-
):
|
|
368
|
-
self._cme_workflow.context["is_assistant_mode_command"] = True
|
|
369
|
-
|
|
370
|
-
# Route based on mode and message type
|
|
371
|
-
if self._run_as_agent and "is_assistant_mode_command" not in self._cme_workflow.context:
|
|
372
|
-
# In agent mode, use workflow tool agent for processing
|
|
373
|
-
last_output = self._process_agent_message(message)
|
|
374
|
-
# elif self._is_mcp_tool_call(message):
|
|
375
|
-
# last_output = self._process_mcp_tool_call(message)
|
|
375
|
+
# Handle Action objects directly
|
|
376
|
+
if isinstance(message, fastworkflow.Action):
|
|
377
|
+
last_output = self._process_action(message)
|
|
376
378
|
else:
|
|
377
|
-
|
|
379
|
+
if ((
|
|
380
|
+
"NLU_Pipeline_Stage" not in self._cme_workflow.context or
|
|
381
|
+
self._cme_workflow.context["NLU_Pipeline_Stage"] == fastworkflow.NLUPipelineStage.INTENT_DETECTION) and
|
|
382
|
+
message.startswith('/')
|
|
383
|
+
):
|
|
384
|
+
self._cme_workflow.context["is_assistant_mode_command"] = True
|
|
385
|
+
|
|
386
|
+
# Route based on mode and message type
|
|
387
|
+
if self._run_as_agent and "is_assistant_mode_command" not in self._cme_workflow.context:
|
|
388
|
+
# In agent mode, use workflow tool agent for processing
|
|
389
|
+
last_output = self._process_agent_message(message)
|
|
390
|
+
else:
|
|
391
|
+
last_output = self._process_message(message)
|
|
378
392
|
|
|
379
393
|
except Empty:
|
|
380
394
|
continue
|
|
@@ -543,13 +557,22 @@ class ChatSession:
|
|
|
543
557
|
|
|
544
558
|
def _process_message(self, message: str) -> fastworkflow.CommandOutput:
|
|
545
559
|
"""Process a single message"""
|
|
546
|
-
#
|
|
547
|
-
|
|
548
|
-
|
|
560
|
+
# Pre-execution trace
|
|
561
|
+
if self.command_trace_queue:
|
|
562
|
+
self.command_trace_queue.put(fastworkflow.CommandTraceEvent(
|
|
563
|
+
direction=fastworkflow.CommandTraceEventDirection.AGENT_TO_WORKFLOW,
|
|
564
|
+
raw_command=message,
|
|
565
|
+
command_name=None,
|
|
566
|
+
parameters=None,
|
|
567
|
+
response_text=None,
|
|
568
|
+
success=None,
|
|
569
|
+
timestamp_ms=int(time.time() * 1000),
|
|
570
|
+
))
|
|
571
|
+
|
|
572
|
+
# Execute command
|
|
549
573
|
command_output = self._CommandExecutor.invoke_command(self, message)
|
|
550
574
|
|
|
551
|
-
#
|
|
552
|
-
# This ensures assistant commands are captured even when interspersed with agent commands
|
|
575
|
+
# Extract response text and parameters for traces
|
|
553
576
|
response_text = ""
|
|
554
577
|
if command_output.command_responses:
|
|
555
578
|
response_text = command_output.command_responses[0].response or ""
|
|
@@ -557,14 +580,30 @@ class ChatSession:
|
|
|
557
580
|
# Convert parameters to dict if it's a Pydantic model or other complex object
|
|
558
581
|
params = command_output.command_parameters or {}
|
|
559
582
|
if hasattr(params, 'model_dump'):
|
|
560
|
-
|
|
583
|
+
params_dict = params.model_dump()
|
|
561
584
|
elif hasattr(params, 'dict'):
|
|
562
|
-
|
|
585
|
+
params_dict = params.dict()
|
|
586
|
+
else:
|
|
587
|
+
params_dict = params
|
|
588
|
+
|
|
589
|
+
# Post-execution trace
|
|
590
|
+
if self.command_trace_queue:
|
|
591
|
+
self.command_trace_queue.put(fastworkflow.CommandTraceEvent(
|
|
592
|
+
direction=fastworkflow.CommandTraceEventDirection.WORKFLOW_TO_AGENT,
|
|
593
|
+
raw_command=None,
|
|
594
|
+
command_name=command_output.command_name or "",
|
|
595
|
+
parameters=params_dict,
|
|
596
|
+
response_text=response_text,
|
|
597
|
+
success=bool(command_output.success),
|
|
598
|
+
timestamp_ms=int(time.time() * 1000),
|
|
599
|
+
))
|
|
563
600
|
|
|
601
|
+
# Record assistant mode trace to action.jsonl (similar to agent mode in workflow_agent.py)
|
|
602
|
+
# This ensures assistant commands are captured even when interspersed with agent commands
|
|
564
603
|
record = {
|
|
565
604
|
"command": message,
|
|
566
605
|
"command_name": command_output.command_name or "",
|
|
567
|
-
"parameters":
|
|
606
|
+
"parameters": params_dict,
|
|
568
607
|
"response": response_text
|
|
569
608
|
}
|
|
570
609
|
|
|
@@ -589,24 +628,54 @@ class ChatSession:
|
|
|
589
628
|
def _process_action(self, action: fastworkflow.Action) -> fastworkflow.CommandOutput:
|
|
590
629
|
"""Process a startup action"""
|
|
591
630
|
workflow = self.get_active_workflow()
|
|
631
|
+
|
|
632
|
+
# Serialize action parameters for trace
|
|
633
|
+
params = action.parameters or {}
|
|
634
|
+
if hasattr(params, 'model_dump'):
|
|
635
|
+
params_dict = params.model_dump()
|
|
636
|
+
elif hasattr(params, 'dict'):
|
|
637
|
+
params_dict = params.dict()
|
|
638
|
+
else:
|
|
639
|
+
params_dict = params
|
|
640
|
+
|
|
641
|
+
# Pre-execution trace: serialize action as raw_command
|
|
642
|
+
raw_command = f"{action.command_name} {json.dumps(params_dict)}"
|
|
643
|
+
if self.command_trace_queue:
|
|
644
|
+
self.command_trace_queue.put(fastworkflow.CommandTraceEvent(
|
|
645
|
+
direction=fastworkflow.CommandTraceEventDirection.AGENT_TO_WORKFLOW,
|
|
646
|
+
raw_command=raw_command,
|
|
647
|
+
command_name=None,
|
|
648
|
+
parameters=None,
|
|
649
|
+
response_text=None,
|
|
650
|
+
success=None,
|
|
651
|
+
timestamp_ms=int(time.time() * 1000),
|
|
652
|
+
))
|
|
653
|
+
|
|
654
|
+
# Execute the action
|
|
592
655
|
command_output = self._CommandExecutor.perform_action(workflow, action)
|
|
593
656
|
|
|
594
|
-
#
|
|
657
|
+
# Extract response text for post-execution trace
|
|
595
658
|
response_text = ""
|
|
596
659
|
if command_output.command_responses:
|
|
597
660
|
response_text = command_output.command_responses[0].response or ""
|
|
598
661
|
|
|
599
|
-
#
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
662
|
+
# Post-execution trace
|
|
663
|
+
if self.command_trace_queue:
|
|
664
|
+
self.command_trace_queue.put(fastworkflow.CommandTraceEvent(
|
|
665
|
+
direction=fastworkflow.CommandTraceEventDirection.WORKFLOW_TO_AGENT,
|
|
666
|
+
raw_command=None,
|
|
667
|
+
command_name=command_output.command_name,
|
|
668
|
+
parameters=params_dict,
|
|
669
|
+
response_text=response_text,
|
|
670
|
+
success=bool(command_output.success),
|
|
671
|
+
timestamp_ms=int(time.time() * 1000),
|
|
672
|
+
))
|
|
605
673
|
|
|
674
|
+
# Record action trace to action.jsonl
|
|
606
675
|
record = {
|
|
607
676
|
"command": "process_action",
|
|
608
677
|
"command_name": action.command_name,
|
|
609
|
-
"parameters":
|
|
678
|
+
"parameters": params_dict,
|
|
610
679
|
"response": response_text
|
|
611
680
|
}
|
|
612
681
|
|
|
@@ -647,7 +716,7 @@ class ChatSession:
|
|
|
647
716
|
def _extract_conversation_summary(self,
|
|
648
717
|
user_query: str, workflow_actions: list[dict[str, str]], final_agent_response: str) -> str:
|
|
649
718
|
"""
|
|
650
|
-
Summarizes conversation based on original user query, workflow actions and
|
|
719
|
+
Summarizes conversation based on original user query, workflow actions and agent response.
|
|
651
720
|
Returns the conversation summary and the log entry
|
|
652
721
|
"""
|
|
653
722
|
# Lets log everything to a file called action_log.jsonl, if it exists
|
|
@@ -613,6 +613,56 @@ class CommandMetadataAPI:
|
|
|
613
613
|
|
|
614
614
|
return "\n".join(combined_lines)
|
|
615
615
|
|
|
616
|
+
@staticmethod
|
|
617
|
+
def get_suggested_commands_metadata(
|
|
618
|
+
subject_workflow_path: str,
|
|
619
|
+
cme_workflow_path: str,
|
|
620
|
+
active_context_name: str,
|
|
621
|
+
suggested_command_names: list[str],
|
|
622
|
+
for_agents: bool = False,
|
|
623
|
+
) -> str:
|
|
624
|
+
"""
|
|
625
|
+
Get metadata display text for ONLY the suggested commands.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
subject_workflow_path: Path to the subject workflow
|
|
629
|
+
cme_workflow_path: Path to the CME workflow
|
|
630
|
+
active_context_name: Active context name
|
|
631
|
+
suggested_command_names: List of suggested command names (can be short names or qualified)
|
|
632
|
+
for_agents: Include agent-specific fields
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
YAML-like formatted string with metadata for suggested commands only
|
|
636
|
+
"""
|
|
637
|
+
if not suggested_command_names:
|
|
638
|
+
return "No suggested commands provided."
|
|
639
|
+
|
|
640
|
+
parts: list[str] = []
|
|
641
|
+
for suggested_cmd in suggested_command_names:
|
|
642
|
+
# Try to get metadata for this command
|
|
643
|
+
# The command name could be qualified (Context/command) or short (command)
|
|
644
|
+
if part := CommandMetadataAPI.get_command_display_text_for_command(
|
|
645
|
+
subject_workflow_path=subject_workflow_path,
|
|
646
|
+
cme_workflow_path=cme_workflow_path,
|
|
647
|
+
active_context_name=active_context_name,
|
|
648
|
+
qualified_command_name=suggested_cmd,
|
|
649
|
+
for_agents=for_agents,
|
|
650
|
+
):
|
|
651
|
+
parts.append(part)
|
|
652
|
+
|
|
653
|
+
if not parts:
|
|
654
|
+
return "No metadata available for suggested commands."
|
|
655
|
+
|
|
656
|
+
# Combine all parts with header
|
|
657
|
+
combined_lines: list[str] = ["Suggested commands with metadata:"]
|
|
658
|
+
for idx, text in enumerate(parts):
|
|
659
|
+
lines = text.splitlines()
|
|
660
|
+
if idx > 0:
|
|
661
|
+
combined_lines.append("") # Blank line separator
|
|
662
|
+
combined_lines.extend(lines)
|
|
663
|
+
|
|
664
|
+
return "\n".join(combined_lines)
|
|
665
|
+
|
|
616
666
|
@staticmethod
|
|
617
667
|
def get_command_display_text_for_command(
|
|
618
668
|
subject_workflow_path: str,
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Intent detection error state agent module for fastWorkflow.
|
|
3
|
+
Specialized agent for handling intent detection errors.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import dspy
|
|
8
|
+
|
|
9
|
+
import fastworkflow
|
|
10
|
+
from fastworkflow.utils.react import fastWorkflowReAct
|
|
11
|
+
from fastworkflow.command_metadata_api import CommandMetadataAPI
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class IntentClarificationAgentSignature(dspy.Signature):
|
|
15
|
+
"""
|
|
16
|
+
Handle intent detection errors by clarifying user intent.
|
|
17
|
+
You are provided with:
|
|
18
|
+
1. The workflow agent's inputs and trajectory - showing what the agent has been trying to do
|
|
19
|
+
2. Suggested commands metadata (for intent ambiguity) or empty (for intent misunderstanding - use show_available_commands tool)
|
|
20
|
+
|
|
21
|
+
Review the agent trajectory to understand the context and what led to this error.
|
|
22
|
+
If suggested_commands_metadata is provided, review it carefully to understand each command's purpose, parameters, and usage.
|
|
23
|
+
If suggested_commands_metadata is empty, use the show_available_commands tool to get the full list of available commands.
|
|
24
|
+
IMPORTANT: When clarifying intent, preserve ALL parameters from the original command.
|
|
25
|
+
Use available tools to resolve ambiguous or misunderstood commands. Use the ask_user tool ONLY as a last resort. Return the complete clarified command with the correct name and all original parameters.
|
|
26
|
+
"""
|
|
27
|
+
original_command = dspy.InputField(desc="The original command with all parameters that caused the error.")
|
|
28
|
+
error_message = dspy.InputField(desc="The intent detection error message from the workflow.")
|
|
29
|
+
agent_inputs = dspy.InputField(desc="The original inputs to the workflow agent.")
|
|
30
|
+
agent_trajectory = dspy.InputField(desc="The workflow agent's trajectory showing all actions taken so far leading to this error.")
|
|
31
|
+
suggested_commands_metadata = dspy.InputField(desc="Metadata for suggested commands (for ambiguity clarification), or empty string (for misunderstanding - use show_available_commands tool instead).")
|
|
32
|
+
clarified_command = dspy.OutputField(desc="The complete command with correct command name AND all original parameters preserved.")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _show_available_commands(chat_session: fastworkflow.ChatSession) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Show available commands to help resolve intent detection errors.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
chat_session: The chat session instance
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
List of available commands
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
current_workflow = chat_session.get_active_workflow()
|
|
47
|
+
return CommandMetadataAPI.get_command_display_text(
|
|
48
|
+
subject_workflow_path=current_workflow.folderpath,
|
|
49
|
+
cme_workflow_path=fastworkflow.get_internal_workflow_path("command_metadata_extraction"),
|
|
50
|
+
active_context_name=current_workflow.current_command_context_name,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _ask_user_for_clarification(
|
|
55
|
+
clarification_request: str,
|
|
56
|
+
chat_session: fastworkflow.ChatSession
|
|
57
|
+
) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Ask user for clarification when intent is unclear.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
clarification_request: The question to ask the user
|
|
63
|
+
chat_session: The chat session instance
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
User's response
|
|
67
|
+
"""
|
|
68
|
+
command_output = fastworkflow.CommandOutput(
|
|
69
|
+
command_responses=[fastworkflow.CommandResponse(response=clarification_request)],
|
|
70
|
+
workflow_name=chat_session.get_active_workflow().folderpath.split('/')[-1]
|
|
71
|
+
)
|
|
72
|
+
chat_session.command_output_queue.put(command_output)
|
|
73
|
+
|
|
74
|
+
user_response = chat_session.user_message_queue.get()
|
|
75
|
+
|
|
76
|
+
# Log to action.jsonl (shared with main agent)
|
|
77
|
+
with open("action.jsonl", "a", encoding="utf-8") as f:
|
|
78
|
+
agent_user_dialog = {
|
|
79
|
+
"intent_clarification_agent": True,
|
|
80
|
+
"agent_query": clarification_request,
|
|
81
|
+
"user_response": user_response
|
|
82
|
+
}
|
|
83
|
+
f.write(json.dumps(agent_user_dialog, ensure_ascii=False) + "\n")
|
|
84
|
+
|
|
85
|
+
return user_response
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def initialize_intent_clarification_agent(
|
|
89
|
+
chat_session: fastworkflow.ChatSession,
|
|
90
|
+
max_iters: int = 20
|
|
91
|
+
):
|
|
92
|
+
"""
|
|
93
|
+
Initialize a specialized agent for handling intent detection errors.
|
|
94
|
+
This agent has a limited tool set and shares traces with the main execution agent.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
chat_session: The chat session instance
|
|
98
|
+
max_iters: Maximum iterations for the agent (default: 10)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
DSPy ReAct agent configured for intent detection error handling
|
|
102
|
+
"""
|
|
103
|
+
if not chat_session:
|
|
104
|
+
raise ValueError("chat_session cannot be null")
|
|
105
|
+
|
|
106
|
+
def show_available_commands() -> str:
|
|
107
|
+
"""
|
|
108
|
+
Show all available commands to help resolve intent ambiguity.
|
|
109
|
+
"""
|
|
110
|
+
return _show_available_commands(chat_session)
|
|
111
|
+
|
|
112
|
+
def ask_user(clarification_request: str) -> str:
|
|
113
|
+
"""
|
|
114
|
+
Ask the user for clarification when the intent is unclear.
|
|
115
|
+
Use this as a last resort when you cannot determine the correct command.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
clarification_request: Clear question to ask the user
|
|
119
|
+
"""
|
|
120
|
+
return _ask_user_for_clarification(clarification_request, chat_session)
|
|
121
|
+
|
|
122
|
+
# Limited tool set for intent detection errors
|
|
123
|
+
tools = [
|
|
124
|
+
show_available_commands,
|
|
125
|
+
ask_user,
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
return fastWorkflowReAct(
|
|
129
|
+
IntentClarificationAgentSignature,
|
|
130
|
+
tools=tools,
|
|
131
|
+
max_iters=max_iters,
|
|
132
|
+
)
|