fastworkflow 2.17.25__py3-none-any.whl → 2.17.27__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,3 +1,8 @@
1
+ # ============================================================================
2
+ # LLM Model Configuration
3
+ # ============================================================================
4
+ # Use direct provider model strings (e.g., mistral/mistral-small-latest)
5
+ # or LiteLLM Proxy model strings (e.g., litellm_proxy/your_model_name)
1
6
  LLM_SYNDATA_GEN=mistral/mistral-small-latest
2
7
  LLM_PARAM_EXTRACTION=mistral/mistral-small-latest
3
8
  LLM_RESPONSE_GEN=mistral/mistral-small-latest
@@ -5,6 +10,20 @@ LLM_PLANNER=mistral/mistral-small-latest
5
10
  LLM_AGENT=mistral/mistral-small-latest
6
11
  LLM_CONVERSATION_STORE=mistral/mistral-small-latest
7
12
 
13
+ # ============================================================================
14
+ # LiteLLM Proxy Configuration (Optional)
15
+ # ============================================================================
16
+ # To route LLM calls through a LiteLLM Proxy, set the model strings above to
17
+ # use the litellm_proxy/ prefix and configure the proxy URL below.
18
+ # Example:
19
+ # LLM_AGENT=litellm_proxy/bedrock_mistral_large_2407
20
+ # LITELLM_PROXY_API_BASE=http://127.0.0.1:4000
21
+ # The proxy API key should be set in fastworkflow.passwords.env
22
+ # LITELLM_PROXY_API_BASE=http://127.0.0.1:4000
23
+
24
+ # ============================================================================
25
+ # Workflow Configuration
26
+ # ============================================================================
8
27
  SPEEDDICT_FOLDERNAME=___workflow_contexts
9
28
  SYNTHETIC_UTTERANCE_GEN_NUMOF_PERSONAS=4
10
29
  SYNTHETIC_UTTERANCE_GEN_UTTERANCES_PER_PERSONA=5
@@ -1,7 +1,20 @@
1
+ # ============================================================================
2
+ # Direct Provider API Keys
3
+ # ============================================================================
1
4
  # Tested with Mistral Small 3.1. A bigger model will produce better results, obviously
5
+ # These keys are used when LLM_* variables use direct provider model strings
6
+ # (e.g., mistral/mistral-small-latest, openai/gpt-4, etc.)
2
7
  LITELLM_API_KEY_SYNDATA_GEN=<API KEY for synthetic data generation model>
3
8
  LITELLM_API_KEY_PARAM_EXTRACTION=<API KEY for parameter extraction model>
4
9
  LITELLM_API_KEY_RESPONSE_GEN=<API KEY for response generation model>
5
10
  LITELLM_API_KEY_PLANNER=<API KEY for the agent's task planner model>
6
11
  LITELLM_API_KEY_AGENT=<API KEY for the agent model>
7
- LITELLM_API_KEY_CONVERSATION_STORE=<API KEY for conversation topic/summary generation model>
12
+ LITELLM_API_KEY_CONVERSATION_STORE=<API KEY for conversation topic/summary generation model>
13
+
14
+ # ============================================================================
15
+ # LiteLLM Proxy API Key (Optional)
16
+ # ============================================================================
17
+ # When using litellm_proxy/ model strings, this shared key is used for all
18
+ # proxied calls. The per-role keys above are ignored for proxied models.
19
+ # Leave commented if your proxy doesn't require authentication.
20
+ # LITELLM_PROXY_API_KEY=<API KEY for LiteLLM Proxy authentication>
@@ -28,15 +28,16 @@ from contextlib import asynccontextmanager
28
28
  import argparse
29
29
 
30
30
  import uvicorn
31
- from jose import JWTError
31
+ from jwt.exceptions import PyJWTError as JWTError
32
32
  from dotenv import dotenv_values
33
33
 
34
34
  import fastworkflow
35
35
  from fastworkflow.utils.logging import logger
36
36
 
37
- from fastapi import FastAPI, HTTPException, status, Depends, Header
37
+ from fastapi import FastAPI, HTTPException, status, Depends, Header, Request
38
38
  from fastapi.middleware.cors import CORSMiddleware
39
39
  from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
40
+ from starlette.middleware.base import BaseHTTPMiddleware
40
41
 
41
42
  from .mcp_specific import setup_mcp
42
43
  from .utils import (
@@ -76,6 +77,85 @@ from .conversation_store import (
76
77
  )
77
78
 
78
79
 
80
+ # ============================================================================
81
+ # Probe Logging Filter Middleware
82
+ # ============================================================================
83
+
84
+ # Paths that should not be logged unless they return non-200 status
85
+ PROBE_PATHS = {"/probes/healthz", "/probes/readyz"}
86
+
87
+
88
+ class ProbeLoggingFilterMiddleware(BaseHTTPMiddleware):
89
+ """
90
+ Middleware to suppress logging for Kubernetes probe endpoints.
91
+
92
+ Probe endpoints (/probes/healthz, /probes/readyz) are called frequently by
93
+ Kubernetes and would generate excessive logs. This middleware only logs
94
+ probe requests when they return a non-200 status code.
95
+ """
96
+
97
+ async def dispatch(self, request: Request, call_next):
98
+ response = await call_next(request)
99
+
100
+ # Only log probe endpoints if they return non-200 status
101
+ if request.url.path in PROBE_PATHS and response.status_code != 200:
102
+ logger.warning(
103
+ f"Probe {request.url.path} returned status {response.status_code}"
104
+ )
105
+
106
+ return response
107
+
108
+
109
+ # ============================================================================
110
+ # Readiness State Tracking
111
+ # ============================================================================
112
+
113
+ class ReadinessState:
114
+ """
115
+ Tracks the readiness state of the application.
116
+
117
+ The application is considered ready when set_ready(True) is called,
118
+ typically after successful initialization in the lifespan startup.
119
+
120
+ Additional debug attributes (is_initialized, workflow_path_valid) are
121
+ retained for production debugging but do not control readiness.
122
+ """
123
+
124
+ def __init__(self):
125
+ self._is_ready = False
126
+ # Debug attributes - do not control readiness, used for diagnostics
127
+ self._is_initialized = False
128
+ self._workflow_path_valid = False
129
+
130
+ def set_ready(self, value: bool = True):
131
+ """Set the main readiness state. Called after successful initialization."""
132
+ self._is_ready = value
133
+
134
+ def set_initialized(self, value: bool = True):
135
+ """Mark FastWorkflow as initialized (for debugging/diagnostics)."""
136
+ self._is_initialized = value
137
+
138
+ def set_workflow_path_valid(self, value: bool = True):
139
+ """Mark workflow path as validated (for debugging/diagnostics)."""
140
+ self._workflow_path_valid = value
141
+
142
+ def is_ready(self) -> bool:
143
+ """Check if the application is ready to serve traffic."""
144
+ return self._is_ready
145
+
146
+ def get_status(self) -> dict:
147
+ """Get detailed readiness status for debugging."""
148
+ return {
149
+ "ready": self._is_ready,
150
+ "fastworkflow_initialized": self._is_initialized,
151
+ "workflow_path_valid": self._workflow_path_valid
152
+ }
153
+
154
+
155
+ # Global readiness state
156
+ readiness_state = ReadinessState()
157
+
158
+
79
159
  # ============================================================================
80
160
  # Session Management
81
161
  # ============================================================================
@@ -162,6 +242,17 @@ async def lifespan(_app: FastAPI):
162
242
 
163
243
  # Configure JWT verification mode based on CLI parameter
164
244
  set_jwt_verification_mode(ARGS.expect_encrypted_jwt)
245
+
246
+ # Mark FastWorkflow as initialized for readiness probe
247
+ readiness_state.set_initialized(True)
248
+
249
+ # Validate workflow path for readiness probe
250
+ if ARGS.workflow_path and os.path.exists(ARGS.workflow_path):
251
+ readiness_state.set_workflow_path_valid(True)
252
+ logger.info(f"Workflow path validated: {ARGS.workflow_path}")
253
+ else:
254
+ logger.warning(f"Workflow path not valid or not found: {ARGS.workflow_path}")
255
+ readiness_state.set_workflow_path_valid(False)
165
256
 
166
257
  async def _active_turn_channel_ids() -> list[str]:
167
258
  active: list[str] = []
@@ -213,6 +304,9 @@ async def lifespan(_app: FastAPI):
213
304
 
214
305
  try:
215
306
  initialize_fastworkflow_on_startup()
307
+ # Mark application as ready to accept traffic
308
+ readiness_state.set_ready(True)
309
+ logger.info("Application ready to accept traffic")
216
310
  yield
217
311
  finally:
218
312
  logger.info("FastWorkflow FastAPI service shutting down...")
@@ -275,8 +369,8 @@ def custom_openapi():
275
369
 
276
370
  # Apply security globally to all endpoints except public ones
277
371
  for path, path_item in openapi_schema["paths"].items():
278
- # Skip endpoints that don't require authentication
279
- if path in ["/initialize", "/refresh_token", "/", "/admin/dump_all_conversations", "/admin/generate_mcp_token"]:
372
+ # Skip endpoints that don't require authentication (including probe endpoints)
373
+ if path in ["/initialize", "/refresh_token", "/", "/admin/dump_all_conversations", "/admin/generate_mcp_token", "/probes/healthz", "/probes/readyz"]:
280
374
  continue
281
375
  for method in path_item:
282
376
  if method in ["get", "post", "put", "delete", "patch"] and "security" not in path_item[method]:
@@ -296,6 +390,9 @@ app.add_middleware(
296
390
  allow_headers=["*"],
297
391
  )
298
392
 
393
+ # Probe logging filter middleware - suppresses logs for successful probe requests
394
+ app.add_middleware(ProbeLoggingFilterMiddleware)
395
+
299
396
  # ============================================================================
300
397
  # Endpoints
301
398
  # ============================================================================
@@ -317,6 +414,91 @@ async def root():
317
414
  """
318
415
 
319
416
 
417
+ # ============================================================================
418
+ # Kubernetes Probe Endpoints
419
+ # ============================================================================
420
+
421
+ @app.get(
422
+ "/probes/healthz",
423
+ operation_id="liveness_probe",
424
+ status_code=status.HTTP_200_OK,
425
+ responses={
426
+ 200: {"description": "Application is alive and running"},
427
+ 503: {"description": "Application is unhealthy"}
428
+ },
429
+ tags=["probes"]
430
+ )
431
+ async def liveness_probe() -> dict:
432
+ """
433
+ Liveness probe endpoint for Kubernetes.
434
+
435
+ Determines whether the container is still running. If this probe fails,
436
+ Kubernetes will restart the container.
437
+
438
+ This endpoint checks basic application health:
439
+ - The FastAPI application is responsive
440
+ - The event loop is processing requests
441
+
442
+ This endpoint is not logged unless it returns a non-200 status code
443
+ to avoid excessive logging from frequent Kubernetes health checks.
444
+
445
+ Returns:
446
+ 200 OK: {"status": "alive"} - Application is running normally
447
+ 503 Service Unavailable: Application is unhealthy
448
+ """
449
+ # Basic liveness check - if we can respond, we're alive
450
+ # The application is considered "live" if it can process HTTP requests
451
+ return {"status": "alive"}
452
+
453
+
454
+ @app.get(
455
+ "/probes/readyz",
456
+ operation_id="readiness_probe",
457
+ status_code=status.HTTP_200_OK,
458
+ responses={
459
+ 200: {"description": "Application is ready to accept traffic"},
460
+ 503: {"description": "Application is not ready to accept traffic"}
461
+ },
462
+ tags=["probes"]
463
+ )
464
+ async def readiness_probe() -> JSONResponse:
465
+ """
466
+ Readiness probe endpoint for Kubernetes.
467
+
468
+ Checks whether the container is ready to accept traffic. Kubernetes only
469
+ routes traffic to containers that pass the readiness check.
470
+
471
+ This endpoint verifies:
472
+ - FastWorkflow has been initialized
473
+ - The configured workflow path is valid and accessible
474
+
475
+ This endpoint is not logged unless it returns a non-200 status code
476
+ to avoid excessive logging from frequent Kubernetes health checks.
477
+
478
+ Returns:
479
+ 200 OK: {"status": "ready", "checks": {...}} - Ready to accept traffic
480
+ 503 Service Unavailable: {"status": "not_ready", "checks": {...}} - Not ready
481
+ """
482
+ status_info = readiness_state.get_status()
483
+
484
+ if readiness_state.is_ready():
485
+ return JSONResponse(
486
+ status_code=status.HTTP_200_OK,
487
+ content={
488
+ "status": "ready",
489
+ "checks": status_info
490
+ }
491
+ )
492
+ else:
493
+ return JSONResponse(
494
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
495
+ content={
496
+ "status": "not_ready",
497
+ "checks": status_info
498
+ }
499
+ )
500
+
501
+
320
502
  @app.post(
321
503
  "/initialize",
322
504
  operation_id="rest_initialize",
@@ -9,8 +9,8 @@ import os
9
9
  from datetime import datetime, timedelta, timezone
10
10
  from typing import Optional
11
11
 
12
- from jose import JWTError, jwt
13
- from jose.constants import ALGORITHMS
12
+ import jwt
13
+ from jwt.exceptions import PyJWTError as JWTError
14
14
  from cryptography.hazmat.primitives import serialization
15
15
  from cryptography.hazmat.primitives.asymmetric import rsa
16
16
  from cryptography.hazmat.backends import default_backend
@@ -19,7 +19,7 @@ from fastworkflow.utils.logging import logger
19
19
 
20
20
 
21
21
  # JWT Configuration (can be made configurable via env vars)
22
- JWT_ALGORITHM = ALGORITHMS.RS256
22
+ JWT_ALGORITHM = "RS256"
23
23
  JWT_ACCESS_TOKEN_EXPIRE_MINUTES = 60 # 1 hour
24
24
  JWT_REFRESH_TOKEN_EXPIRE_DAYS = 30 # 30 days
25
25
  JWT_ISSUER = "fastworkflow-api"
@@ -274,7 +274,7 @@ def verify_token(token: str, expected_type: str = "access") -> dict:
274
274
  # Trusted network mode: decode without verification (accepts both unsigned and signed tokens)
275
275
  try:
276
276
  # Use unverified decoding - works for any JWT regardless of algorithm or signing
277
- payload = jwt.get_unverified_claims(token)
277
+ payload = jwt.decode(token, options={"verify_signature": False})
278
278
  except Exception as e:
279
279
  logger.warning(f"Token decoding failed: {e}")
280
280
  raise JWTError(f"Failed to decode token: {e}") from e
@@ -332,7 +332,7 @@ def get_token_expiry(token: str) -> Optional[datetime]:
332
332
  """
333
333
  try:
334
334
  # Decode without verification (just to inspect claims)
335
- payload = jwt.get_unverified_claims(token)
335
+ payload = jwt.decode(token, options={"verify_signature": False})
336
336
  if exp_timestamp := payload.get("exp"):
337
337
  return datetime.fromtimestamp(exp_timestamp, tz=timezone.utc)
338
338
  except Exception as e:
@@ -7,7 +7,7 @@ from typing import Any, Optional
7
7
 
8
8
  from fastapi import HTTPException, status, Depends
9
9
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
10
- from jose import JWTError
10
+ from jwt.exceptions import PyJWTError as JWTError
11
11
  from pydantic import BaseModel, field_validator
12
12
 
13
13
  import fastworkflow
@@ -74,11 +74,15 @@ class CommandsSystemPreludeAdapter(dspy.ChatAdapter):
74
74
  Returns:
75
75
  Formatted messages with commands injected into system message
76
76
  """
77
- # Call the base adapter's format method
78
- formatted = self.base.format(signature, demos, inputs)
79
-
80
- # Check if available_commands is in inputs
77
+ # Extract available_commands before passing to base adapter
81
78
  cmds = inputs.get("available_commands")
79
+
80
+ # Create a copy of inputs without available_commands to avoid including it in user message
81
+ inputs_for_base = {k: v for k, v in inputs.items() if k != "available_commands"}
82
+
83
+ # Call the base adapter's format method with filtered inputs
84
+ formatted = self.base.format(signature, demos, inputs_for_base)
85
+
82
86
  if not cmds:
83
87
  return formatted
84
88
 
@@ -6,11 +6,65 @@ import fastworkflow
6
6
  from fastworkflow.utils.logging import logger
7
7
 
8
8
  def get_lm(model_env_var: str, api_key_env_var: Optional[str] = None, **kwargs):
9
- """get the dspy lm object"""
9
+ """
10
+ Get the dspy LM object.
11
+
12
+ Supports LiteLLM Proxy routing: if the model string starts with 'litellm_proxy/',
13
+ the call is routed through the LiteLLM Proxy using LITELLM_PROXY_API_BASE and
14
+ LITELLM_PROXY_API_KEY environment variables.
15
+
16
+ Args:
17
+ model_env_var: Name of the environment variable containing the model string
18
+ (e.g., 'LLM_AGENT', 'LLM_PARAM_EXTRACTION').
19
+ api_key_env_var: Name of the environment variable containing the API key
20
+ for direct provider calls. Ignored for litellm_proxy/ models.
21
+ **kwargs: Additional keyword arguments passed to dspy.LM().
22
+
23
+ Returns:
24
+ dspy.LM: Configured language model instance.
25
+
26
+ Raises:
27
+ ValueError: If model is not set, or if using litellm_proxy/ without
28
+ LITELLM_PROXY_API_BASE configured.
29
+
30
+ Example:
31
+ # Direct provider call (existing behavior):
32
+ # LLM_AGENT=mistral/mistral-small-latest
33
+ # LITELLM_API_KEY_AGENT=sk-...
34
+ lm = get_lm("LLM_AGENT", "LITELLM_API_KEY_AGENT")
35
+
36
+ # LiteLLM Proxy call:
37
+ # LLM_AGENT=litellm_proxy/bedrock_mistral_large_2407
38
+ # LITELLM_PROXY_API_BASE=http://127.0.0.1:4000
39
+ # LITELLM_PROXY_API_KEY=proxy-key-...
40
+ lm = get_lm("LLM_AGENT", "LITELLM_API_KEY_AGENT") # api_key_env_var is ignored for proxy
41
+ """
10
42
  model = fastworkflow.get_env_var(model_env_var)
11
43
  if not model:
12
- logger.critical(f"Critical Error:DSPy Language Model not provided. Set {model_env_var} environment variable.")
44
+ logger.critical(f"Critical Error: DSPy Language Model not provided. Set {model_env_var} environment variable.")
13
45
  raise ValueError(f"DSPy Language Model not provided. Set {model_env_var} environment variable.")
46
+
47
+ # Check if this is a LiteLLM Proxy call
48
+ if model.startswith("litellm_proxy/"):
49
+ # Route through LiteLLM Proxy
50
+ proxy_api_base = fastworkflow.get_env_var("LITELLM_PROXY_API_BASE")
51
+ if not proxy_api_base:
52
+ raise ValueError(
53
+ f"Model '{model}' uses litellm_proxy/ prefix but LITELLM_PROXY_API_BASE is not set. "
54
+ "Set LITELLM_PROXY_API_BASE to your LiteLLM Proxy URL (e.g., http://127.0.0.1:4000)."
55
+ )
56
+
57
+ # Get optional proxy API key (allows no-auth proxies when empty/not set)
58
+ proxy_api_key = fastworkflow.get_env_var("LITELLM_PROXY_API_KEY", default=None)
59
+
60
+ logger.debug(f"Routing {model_env_var} through LiteLLM Proxy at {proxy_api_base}")
61
+
62
+ if proxy_api_key:
63
+ return dspy.LM(model=model, api_base=proxy_api_base, api_key=proxy_api_key, **kwargs)
64
+ else:
65
+ return dspy.LM(model=model, api_base=proxy_api_base, **kwargs)
66
+
67
+ # Direct provider call (existing behavior)
14
68
  api_key = fastworkflow.get_env_var(api_key_env_var) if api_key_env_var else None
15
69
  return dspy.LM(model=model, api_key=api_key, **kwargs) if api_key else dspy.LM(model=model, **kwargs)
16
70
 
@@ -47,7 +47,7 @@ class FormatterNs(logging.Formatter):
47
47
 
48
48
  logging.setLogRecordFactory(LogRecordNs)
49
49
 
50
- LOG_FORMAT = "%(asctime)s - %(levelname)s - %(filename)s-%(funcName)s - %(message)s"
50
+ LOG_FORMAT = "%(levelname)s: %(message)s - %(asctime)s - %(filename)s-%(funcName)s"
51
51
  log_formatter = FormatterNs(LOG_FORMAT)
52
52
 
53
53
  if log_level := get_env_variable("LOG_LEVEL", "INFO"):
@@ -98,7 +98,7 @@ pytest_assertion_logger.propagate = (
98
98
  pytest_assertion_logger.handlers.clear()
99
99
  ch = logging.StreamHandler()
100
100
  ch.setLevel(logging.DEBUG)
101
- ch.setFormatter(FormatterNs("%(asctime)s - %(levelname)s - %(message)s"))
101
+ ch.setFormatter(FormatterNs("%(levelname)s: %(message)s - %(asctime)s"))
102
102
  pytest_assertion_logger.addHandler(ch)
103
103
 
104
104
  logging.getLogger("dspy").setLevel(logging.ERROR)
@@ -72,14 +72,13 @@ class fastWorkflowReAct(Module):
72
72
  instr.extend(f"({idx + 1}) {tool}" for idx, tool in enumerate(tools.values()))
73
73
  instr.append("When providing `next_tool_args`, the value inside the field must be in JSON format")
74
74
 
75
- # Build the ReAct signature with trajectory and available_commands inputs.
75
+ # Build the ReAct signature with trajectory input.
76
76
  # available_commands is injected into system message by CommandsSystemPreludeAdapter
77
77
  # (see fastworkflow/utils/chat_adapter.py) and is NOT included in the trajectory
78
78
  # formatting to avoid token bloat across iterations.
79
79
  react_signature = (
80
80
  dspy.Signature({**signature.input_fields}, "\n".join(instr))
81
81
  .append("trajectory", dspy.InputField(), type_=str)
82
- .append("available_commands", dspy.InputField(), type_=str)
83
82
  .append("next_thought", dspy.OutputField(), type_=str)
84
83
  .append("next_tool_name", dspy.OutputField(), type_=Literal[tuple(tools.keys())])
85
84
  .append("next_tool_args", dspy.OutputField(), type_=dict[str, Any])
@@ -88,7 +87,7 @@ class fastWorkflowReAct(Module):
88
87
  fallback_signature = dspy.Signature(
89
88
  {**signature.input_fields, **signature.output_fields},
90
89
  signature.instructions,
91
- ).append("trajectory", dspy.InputField(), type_=str).append("available_commands", dspy.InputField(), type_=str)
90
+ ).append("trajectory", dspy.InputField(), type_=str)
92
91
 
93
92
  self.tools = tools
94
93
  self.react = dspy.Predict(react_signature)
@@ -12,7 +12,7 @@ from fastworkflow.utils.logging import logger
12
12
  from fastworkflow.utils import dspy_utils
13
13
  from fastworkflow.command_metadata_api import CommandMetadataAPI
14
14
  from fastworkflow.utils.react import fastWorkflowReAct
15
-
15
+ from fastworkflow.utils.chat_adapter import CommandsSystemPreludeAdapter
16
16
 
17
17
  class WorkflowAgentSignature(dspy.Signature):
18
18
  """
@@ -110,8 +110,7 @@ def _execute_workflow_query(command: str, chat_session_obj: fastworkflow.ChatSes
110
110
 
111
111
  # Handle intent ambiguity clarification state with specialized agent
112
112
  if nlu_stage == fastworkflow.NLUPipelineStage.INTENT_AMBIGUITY_CLARIFICATION:
113
- if intent_agent := chat_session_obj.intent_clarification_agent:
114
- from fastworkflow.utils.chat_adapter import CommandsSystemPreludeAdapter
113
+ if intent_agent := chat_session_obj.intent_clarification_agent:
115
114
  # Use CommandsSystemPreludeAdapter specifically for workflow agent calls
116
115
  agent_adapter = CommandsSystemPreludeAdapter()
117
116
 
@@ -321,8 +320,7 @@ def build_query_with_next_steps(user_query: str,
321
320
  Avoid specifying 'ask user' because 9 times out of 10, you can find the information via available commands.
322
321
  """
323
322
  user_query: str = dspy.InputField()
324
- available_commands: list[str] = dspy.InputField()
325
- next_steps: list[str] = dspy.OutputField(desc="task descriptions as short sentences")
323
+ next_steps: str = dspy.OutputField(desc="task descriptions as a numbered list of short sentences separated by line breaks")
326
324
 
327
325
  class TaskPlannerWithTrajectoryAndAgentInputsSignature(dspy.Signature):
328
326
  """
@@ -333,8 +331,7 @@ def build_query_with_next_steps(user_query: str,
333
331
  agent_inputs: dict = dspy.InputField()
334
332
  agent_trajectory: dict = dspy.InputField()
335
333
  user_response: str = dspy.InputField()
336
- available_commands: list[str] = dspy.InputField()
337
- next_steps: list[str] = dspy.OutputField(desc="task descriptions as short sentences")
334
+ next_steps: str = dspy.OutputField(desc="task descriptions as a numbered list of short sentences separated by line breaks")
338
335
 
339
336
  current_workflow = chat_session_obj.get_active_workflow()
340
337
  available_commands = CommandMetadataAPI.get_command_display_text(
@@ -344,26 +341,28 @@ def build_query_with_next_steps(user_query: str,
344
341
  )
345
342
 
346
343
  planner_lm = dspy_utils.get_lm("LLM_PLANNER", "LITELLM_API_KEY_PLANNER")
347
- with dspy.context(lm=planner_lm):
344
+ agent_adapter = CommandsSystemPreludeAdapter()
345
+ with dspy.context(lm=planner_lm, adapter=agent_adapter):
348
346
  if with_agent_inputs_and_trajectory:
349
347
  workflow_tool_agent = chat_session_obj.workflow_tool_agent
350
348
  task_planner_func = dspy.ChainOfThought(TaskPlannerWithTrajectoryAndAgentInputsSignature)
349
+ cleaned_agent_inputs = {k: v for k, v in workflow_tool_agent.inputs.items() if k != "available_commands"}
351
350
  prediction = task_planner_func(
352
- agent_inputs = workflow_tool_agent.inputs,
351
+ agent_inputs = cleaned_agent_inputs,
353
352
  agent_trajectory = workflow_tool_agent.current_trajectory,
354
353
  user_response = user_query,
355
- available_commands=available_commands)
354
+ available_commands=available_commands) # Note that this is not part of the signature. It is extra metadata that will be picked up by the CommandsSystemPreludeAdapter
356
355
  else:
357
356
  task_planner_func = dspy.ChainOfThought(TaskPlannerSignature)
358
357
  prediction = task_planner_func(
359
358
  user_query=user_query,
360
- available_commands=available_commands)
359
+ available_commands=available_commands) # Note that this is not part of the signature. It is extra metadata that will be picked up by the CommandsSystemPreludeAdapter
361
360
 
362
361
  if not prediction.next_steps:
363
362
  return user_query
364
363
 
365
- steps_list = '\n'.join([f'{i + 1}. {task}' for i, task in enumerate(prediction.next_steps)])
366
- user_query_and_next_steps = f"{user_query}\n\nExecute these next steps:\n{steps_list}"
364
+ steps_formatted = " ".join(prediction.next_steps.split())
365
+ user_query_and_next_steps = f"{user_query}\n\nExecute these next steps:\n{steps_formatted}"
367
366
  return (
368
367
  f'User Query:\n{user_query_and_next_steps}'
369
368
  if with_agent_inputs_and_trajectory else
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastworkflow
3
- Version: 2.17.25
3
+ Version: 2.17.27
4
4
  Summary: A framework for rapidly building large-scale, deterministic, interactive workflows with a fault-tolerant, conversational UX
5
5
  License: Apache-2.0
6
6
  Keywords: fastworkflow,ai,workflow,llm,openai
@@ -13,24 +13,31 @@ Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Provides-Extra: fastapi
15
15
  Provides-Extra: training
16
+ Requires-Dist: aiohttp (>=3.13.3)
16
17
  Requires-Dist: colorama (>=0.4.6,<0.5.0)
17
18
  Requires-Dist: datasets (>=4.0.0,<5.0.0) ; extra == "training"
18
19
  Requires-Dist: dspy (>=3.0.1,<4.0.0)
19
20
  Requires-Dist: fastapi (>=0.120.1) ; extra == "fastapi"
20
21
  Requires-Dist: fastapi-mcp (>=0.4.0,<0.5.0) ; extra == "fastapi"
22
+ Requires-Dist: filelock (>=3.20.1)
21
23
  Requires-Dist: libcst (>=1.8.2,<2.0.0)
22
- Requires-Dist: litellm[proxy] (>=1.80.5,<2.0.0)
24
+ Requires-Dist: litellm[proxy] (>=1.81.4,<2.0.0)
23
25
  Requires-Dist: mmh3 (>=5.1.0,<6.0.0)
24
26
  Requires-Dist: openai (>=2.8.0)
27
+ Requires-Dist: orjson (>=3.9.15)
25
28
  Requires-Dist: prompt_toolkit (>=3.0.43,<4.0.0)
29
+ Requires-Dist: pyasn1 (>=0.6.2)
26
30
  Requires-Dist: pydantic (>=2.9.2,<3.0.0)
31
+ Requires-Dist: pyjwt[crypto] (>=2.4.0) ; extra == "fastapi"
32
+ Requires-Dist: pynacl (>=1.6.2)
27
33
  Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
28
- Requires-Dist: python-jose[cryptography] (>=3.3.0,<4.0.0) ; extra == "fastapi"
29
34
  Requires-Dist: python-levenshtein (>=0.27.1,<0.28.0)
30
35
  Requires-Dist: scikit-learn (>=1.6.1,<2.0.0)
31
36
  Requires-Dist: speedict (>=0.3.12,<0.4.0)
37
+ Requires-Dist: starlette (>=0.49.1)
32
38
  Requires-Dist: torch (>=2.7.1,<3.0.0)
33
39
  Requires-Dist: transformers (>=4.48.2,<5.0.0)
40
+ Requires-Dist: urllib3 (>=2.6.0)
34
41
  Requires-Dist: uvicorn (>=0.31.1,<0.32.0) ; extra == "fastapi"
35
42
  Project-URL: homepage, https://github.com/radiantlogicinc/fastworkflow
36
43
  Project-URL: repository, https://github.com/radiantlogicinc/fastworkflow
@@ -55,8 +62,22 @@ While [DSPy](https://dspy.ai) ([Why DSPy](https://x.com/lateinteraction/status/1
55
62
 
56
63
  ### Why fastWorkflow?
57
64
 
58
- - ✅ **Unlimited Tool Scaling**: fastworkflow organizes tools into context hierarchies so use any number of tools without sacrificing performance or efficiency
59
65
  - ✅ **Cost-Effective Performance**: fastWorkFlow with small, free models can match the quality of large expensive models
66
+
67
+ <p align="center">
68
+ <table>
69
+ <tr>
70
+ <td align="center" width="50%">
71
+ <img src="fastWorkflow - Tau Bench Retail.jpg" alt="fastWorkflow - Tau Bench Retail" style="max-width: 100%; height: auto;"/>
72
+ </td>
73
+ <td align="center" width="50%">
74
+ <img src="fastWorkflow - TauBench Airline.jpg" alt="fastWorkflow - TauBench Airline" style="max-width: 100%; height: auto;"/>
75
+ </td>
76
+ </tr>
77
+ </table>
78
+ </p>
79
+
80
+ - ✅ **Unlimited Tool Scaling**: fastworkflow organizes tools into context hierarchies so use any number of tools without sacrificing performance or efficiency
60
81
  - ✅ **Reliable Tool Execution**: fastworkflow validation pipeline virtually eliminates incorrect tool calling or parameter extraction, ensuring a reliable tool response
61
82
  - ✅ **Adaptive Learning**: 1-shot learning from intent detection mistakes. It learns your conversational vocabulary as you interact with it
62
83
  - ✅ **Interface Flexibility**: Support programmatic, assistant-driven and agent-driven interfaces with the same codebase
@@ -592,6 +613,7 @@ This single command will generate the `greet.py` command, `get_properties` and `
592
613
  | `LLM_PLANNER` | LiteLLM model string for the agent's task planner | `run` (agent mode) | `mistral/mistral-small-latest` |
593
614
  | `LLM_AGENT` | LiteLLM model string for the DSPy agent | `run` (agent mode) | `mistral/mistral-small-latest` |
594
615
  | `LLM_CONVERSATION_STORE` | LiteLLM model string for conversation topic/summary generation | FastAPI service | `mistral/mistral-small-latest` |
616
+ | `LITELLM_PROXY_API_BASE` | URL of your LiteLLM Proxy server | When using `litellm_proxy/` models | *not set* |
595
617
  | `NOT_FOUND` | Placeholder value for missing parameters during extraction | Always | `"NOT_FOUND"` |
596
618
  | `MISSING_INFORMATION_ERRMSG` | Error message prefix for missing parameters | Always | `"Missing required..."` |
597
619
  | `INVALID_INFORMATION_ERRMSG` | Error message prefix for invalid parameters | Always | `"Invalid information..."` |
@@ -606,10 +628,35 @@ This single command will generate the `greet.py` command, `get_properties` and `
606
628
  | `LITELLM_API_KEY_PLANNER`| API key for the `LLM_PLANNER` model | `run` (agent mode) | *required* |
607
629
  | `LITELLM_API_KEY_AGENT`| API key for the `LLM_AGENT` model | `run` (agent mode) | *required* |
608
630
  | `LITELLM_API_KEY_CONVERSATION_STORE`| API key for the `LLM_CONVERSATION_STORE` model | FastAPI service | *required* |
631
+ | `LITELLM_PROXY_API_KEY`| Shared API key for authenticating with LiteLLM Proxy | When using `litellm_proxy/` models | *optional* |
609
632
 
610
633
  > [!tip]
611
634
  > The example workflows are configured to use Mistral's models by default. You can get a free API key from [Mistral AI](https://mistral.ai) that works with the `mistral-small-latest` model.
612
635
 
636
+ ### Using LiteLLM Proxy
637
+
638
+ FastWorkflow supports routing LLM calls through a [LiteLLM Proxy](https://docs.litellm.ai/docs/simple_proxy) server. This is useful when you want to:
639
+ - Centralize API key management
640
+ - Use a unified endpoint for multiple LLM providers
641
+ - Route requests through a corporate proxy with custom configurations
642
+
643
+ To use LiteLLM Proxy, set your model strings to use the `litellm_proxy/` prefix and configure the proxy URL:
644
+
645
+ ```
646
+ # In fastworkflow.env - use the litellm_proxy/ prefix for model names
647
+ LLM_AGENT=litellm_proxy/bedrock_mistral_large_2407
648
+ LLM_PARAM_EXTRACTION=litellm_proxy/bedrock_mistral_large_2407
649
+ LITELLM_PROXY_API_BASE=http://127.0.0.1:4000
650
+
651
+ # In fastworkflow.passwords.env - shared key for proxy authentication
652
+ LITELLM_PROXY_API_KEY=your-proxy-api-key
653
+ ```
654
+
655
+ The model name after `litellm_proxy/` (e.g., `bedrock_mistral_large_2407`) is passed to your proxy server, which routes it to the actual provider based on its configuration.
656
+
657
+ > [!note]
658
+ > When using `litellm_proxy/` models, the per-role API keys (`LITELLM_API_KEY_*`) are ignored. All proxied calls use the shared `LITELLM_PROXY_API_KEY` instead. You can mix proxied and direct models - only models with the `litellm_proxy/` prefix are routed through the proxy.
659
+
613
660
  ---
614
661
 
615
662
  ## Troubleshooting / FAQ
@@ -50,8 +50,8 @@ fastworkflow/examples/extended_workflow_example/_commands/generate_report.py,sha
50
50
  fastworkflow/examples/extended_workflow_example/_commands/startup.py,sha256=V5Q29148SvXw6i3i0pKTuNWsv2xnkUMsHHuzt1ndxro,1028
51
51
  fastworkflow/examples/extended_workflow_example/simple_workflow_template.json,sha256=A-dAl5iD9ehdMGGn05O2Kjwq6ZetqQjAGzlM1st0K9U,1237
52
52
  fastworkflow/examples/extended_workflow_example/workflow_inheritance_model.json,sha256=TBk272pqfyRKzm4T-I6_nGfbcdmEzjwon7kFPWtgyhw,81
53
- fastworkflow/examples/fastworkflow.env,sha256=mLI1fWqkzjcp9uzfHw81mlOx4JFb8Ch_TBy8dX1Dsok,675
54
- fastworkflow/examples/fastworkflow.passwords.env,sha256=9bI62EokFWT_YPcO0UAvO1ZTG2wM76Jbe5cKE7_KTRg,517
53
+ fastworkflow/examples/fastworkflow.env,sha256=fqzEafhyy4TfJ-tpDq24ZASi1sDEmDQTNcBnDK6kC4o,1756
54
+ fastworkflow/examples/fastworkflow.passwords.env,sha256=DDJ0ZWksEQX6FxGvjVh8VsWOKDEomCufrsyeOKTBaHU,1311
55
55
  fastworkflow/examples/hello_world/_commands/README.md,sha256=pYOTGqVx41ZIuNc6hPTEJzNcMQ2Vwx3PN74ifSlayvU,1297
56
56
  fastworkflow/examples/hello_world/_commands/add_two_numbers.py,sha256=0lFGK1llT6u6fByvzCDPdegjY6gWcerM2cvxVSo7lIw,2232
57
57
  fastworkflow/examples/hello_world/_commands/context_inheritance_model.json,sha256=RBNvo1WzZ4oRRq0W9-hknpT7T8If536DEMBg9hyq_4o,2
@@ -148,37 +148,37 @@ fastworkflow/run/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
148
148
  fastworkflow/run/__main__.py,sha256=kHgLI5kQ__4ITNFw7QJdv5u8nmmxbCyLsaiSde6Hnjc,12199
149
149
  fastworkflow/run_fastapi_mcp/README.md,sha256=dAmG2KF-9mqSjyIPSA9vhUit-DjsDH6WJUDCkQ3C1is,11943
150
150
  fastworkflow/run_fastapi_mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
151
- fastworkflow/run_fastapi_mcp/__main__.py,sha256=PRW-0NYt0_SD5uSc4EHBcvjVE1E33rH8kg3iOYCJIH8,53881
151
+ fastworkflow/run_fastapi_mcp/__main__.py,sha256=vPdg7vEqxBvMDAAErk0MU921z9jXIRNqSuAPVGbBqKE,60540
152
152
  fastworkflow/run_fastapi_mcp/conversation_store.py,sha256=2qnNLO_RVHznbIzTjpdff7szsrGyr1FVt1spcKvkrKk,13534
153
- fastworkflow/run_fastapi_mcp/jwt_manager.py,sha256=o3JLV71WiKNhr61KFIrYDnYQvvNYrqhSqEnsWNBUya4,12480
153
+ fastworkflow/run_fastapi_mcp/jwt_manager.py,sha256=XHImakUgetCHRHwyacsWUtv0dhlrZtFF6vdastO6XEc,12507
154
154
  fastworkflow/run_fastapi_mcp/mcp_specific.py,sha256=RdOPcPn68KlxNSM9Vb2yeYEDNGoNTcKZq-AC0cd86cw,4506
155
155
  fastworkflow/run_fastapi_mcp/redoc_2_standalone_html.py,sha256=oYWn30O-xKX6pVjunCeLupyOM2DbeZ3QgFj-F2LalOE,1191
156
- fastworkflow/run_fastapi_mcp/utils.py,sha256=SX6meWba0T-iYn7YmEajbwJrijfVVUuYGv4usDXzA2c,19589
156
+ fastworkflow/run_fastapi_mcp/utils.py,sha256=-wqhC0fh3qROaup4lXjxObC9Ug296lRDyGZzZs65sQc,19613
157
157
  fastworkflow/train/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
158
158
  fastworkflow/train/__main__.py,sha256=m4v9uczmZ58EfNlJKc-cewMjPeltLL7tNRKotYtig3o,9532
159
159
  fastworkflow/train/generate_synthetic.py,sha256=ingoGxpwlaHGM9WHeK1xULEZntr5HBmQohyLtpqVTD0,5917
160
160
  fastworkflow/user_message_queues.py,sha256=svbuFxQ16q6Tz6urPWfD4IEsOTMxtS1Kc1PP8EE8AWg,1422
161
161
  fastworkflow/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
162
- fastworkflow/utils/chat_adapter.py,sha256=-U5JFiPynDhSYXJ75wdY0EA-hH8QPaq1bzA6ju4ZnVc,4090
162
+ fastworkflow/utils/chat_adapter.py,sha256=NVIvSmd0L4QNYTg2oEZzR1iCdPjti0C0ZMffaylfBx8,4319
163
163
  fastworkflow/utils/context_utils.py,sha256=mjYVzNJCmimNMmBdOKfzFeDSws_oAADAwcfz_N6sR7M,749
164
164
  fastworkflow/utils/dspy_cache_utils.py,sha256=OP2IsWPMGCdhjC-4iRqggWgTEfvPxFN_78tV1_C6uHY,3725
165
165
  fastworkflow/utils/dspy_logger.py,sha256=NS40fYl-J-vps82BUh9D8kqv5dP3_qAY78HZWyZemEA,6571
166
- fastworkflow/utils/dspy_utils.py,sha256=Gl7hh3chxAKfPTE4uuHkfhHcGXuwM7paWUMSgzcMqh0,5392
166
+ fastworkflow/utils/dspy_utils.py,sha256=eFpU6jggaE9SGXO88Imxye6Q_EYsU0aymuFCGOswDdo,7800
167
167
  fastworkflow/utils/env.py,sha256=2E9sev6kWEHP0jx1gs1Kv2HJAjr_mb8nyIPzWpRBU08,787
168
168
  fastworkflow/utils/fuzzy_match.py,sha256=9NRvgrhHezslGQdquFeWXxc2oE1eNYz4NFMEtsSeXMw,2521
169
169
  fastworkflow/utils/generate_param_examples.py,sha256=K0x1Zwe82xqhKA15AYTodWg7mquXsobXtqtZT-B5QAE,25581
170
- fastworkflow/utils/logging.py,sha256=2SA-04fg7Lx_vGf980tfCOGDQxBvU9X6Vbhv47rbdaw,4110
170
+ fastworkflow/utils/logging.py,sha256=CsPlhqtR2_HpWNk4iYVKSRtu_xeij6SQKy48xlnKEmI,4116
171
171
  fastworkflow/utils/parameterize_func_decorator.py,sha256=V6YJnishWRCdwiBQW6P17hmGGrga0Empk-AN5Gm7iMk,633
172
172
  fastworkflow/utils/pydantic_model_2_dspy_signature_class.py,sha256=w1pvl8rJq48ulFwaAtBgfXYn_SBIDBgq1aLMUg1zJn8,12875
173
173
  fastworkflow/utils/python_utils.py,sha256=KMxktfIVOre7qkLhd80Ig39g313EMx_I_oHSa6sC5wI,8512
174
- fastworkflow/utils/react.py,sha256=FGDnzIPKSTwXOCrzUVluFtkZ06lVjgMdB-YQ8jhggZU,13065
174
+ fastworkflow/utils/react.py,sha256=dmDn0huU_rp6z4p-gKwag5Btlmcb9ZsnukO1tXNFTGQ,12910
175
175
  fastworkflow/utils/signatures.py,sha256=ddcwCLNF_5dpItvcHdkZ0WBMse7CaqYpAyg6WwoJZPo,33310
176
176
  fastworkflow/utils/startup_progress.py,sha256=9icSdnpFAxzIq0sUliGpNaH0Efvrt5lDtGfURV5BD98,3539
177
177
  fastworkflow/workflow.py,sha256=37gn7e3ct-gdGw43zS6Ab_ADoJJBO4eJW2PywfUpjEg,18825
178
- fastworkflow/workflow_agent.py,sha256=vdGoeiG7xIsG7rhqjdIwveCkPDvs_bb3dE-Pw-unYMA,18848
178
+ fastworkflow/workflow_agent.py,sha256=jCvMyz5mLr8UX5QN1ssWebs4f24XhirjGkoJpsS-qZ0,19202
179
179
  fastworkflow/workflow_inheritance_model.py,sha256=Pp-qSrQISgPfPjJVUfW84pc7HLmL2evuq0UVIYR51K0,7974
180
- fastworkflow-2.17.25.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
181
- fastworkflow-2.17.25.dist-info/METADATA,sha256=VSj04l_4EOQ6_kwjGhBsPdiy7elZmaT234SA5DJ1hHM,30984
182
- fastworkflow-2.17.25.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
183
- fastworkflow-2.17.25.dist-info/entry_points.txt,sha256=m8HqoPzCyaZLAx-V5X8MJgw3Lx3GiPDlxNEZ7K-Gb-U,54
184
- fastworkflow-2.17.25.dist-info/RECORD,,
180
+ fastworkflow-2.17.27.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
181
+ fastworkflow-2.17.27.dist-info/METADATA,sha256=DswdUGq2rnGkYoe_lGjUqtJYmm4IlUyjU3zpVqU-CmY,33130
182
+ fastworkflow-2.17.27.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
183
+ fastworkflow-2.17.27.dist-info/entry_points.txt,sha256=m8HqoPzCyaZLAx-V5X8MJgw3Lx3GiPDlxNEZ7K-Gb-U,54
184
+ fastworkflow-2.17.27.dist-info/RECORD,,