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.
@@ -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
- # Otherwise use the LLM-based extraction
63
- new_params = input_for_param_extraction.extract_parameters(
64
- command_parameters_class,
65
- self.command_name,
66
- app_workflow_folderpath)
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
  """
@@ -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
- if ((
364
- "NLU_Pipeline_Stage" not in self._cme_workflow.context or
365
- self._cme_workflow.context["NLU_Pipeline_Stage"] == fastworkflow.NLUPipelineStage.INTENT_DETECTION) and
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
- last_output = self._process_message(message)
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
- # Use our specialized profiling method
547
- # command_output = self.profile_invoke_command(message)
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
- # Record assistant mode trace to action.jsonl (similar to agent mode in workflow_agent.py)
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
- params = params.model_dump()
583
+ params_dict = params.model_dump()
561
584
  elif hasattr(params, 'dict'):
562
- params = params.dict()
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": params,
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
- # Record action trace to action.jsonl
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
- # Convert parameters to dict if it's a Pydantic model or other complex object
600
- params = action.parameters or {}
601
- if hasattr(params, 'model_dump'):
602
- params = params.model_dump()
603
- elif hasattr(params, 'dict'):
604
- params = params.dict()
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": params,
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 agentt response.
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
+ )