fastworkflow 2.17.26__py3-none-any.whl → 2.17.28__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/__init__.py CHANGED
@@ -132,6 +132,12 @@ def init(env_vars: dict):
132
132
  global _env_vars, CommandContextModel, RoutingDefinition, RoutingRegistry, ModelPipelineRegistry
133
133
  _env_vars = env_vars
134
134
 
135
+ # Reconfigure log level from env_vars (dotenv files) if LOG_LEVEL is specified
136
+ # This allows LOG_LEVEL to be set in fastworkflow.env files, not just OS environment
137
+ if log_level := env_vars.get("LOG_LEVEL"):
138
+ from .utils.logging import reconfigure_log_level
139
+ reconfigure_log_level(log_level)
140
+
135
141
  # init before importing other modules so env vars are available
136
142
  from .command_context_model import CommandContextModel as CommandContextModelClass
137
143
  from .command_routing import RoutingDefinition as RoutingDefinitionClass
@@ -16,7 +16,7 @@ def get_import_block():
16
16
  f"from fastworkflow.train.generate_synthetic import generate_diverse_utterances\n"
17
17
  f"from fastworkflow.utils.context_utils import list_context_names\n"
18
18
  f"from typing import Any, Dict, Optional\n"
19
- f"from pydantic import BaseModel, Field\n"
19
+ f"from pydantic import BaseModel, Field, ConfigDict\n"
20
20
  )
21
21
 
22
22
  def create_function_command_file(function_info: FunctionInfo, output_dir: str, file_name: str = None, source_dir: str = None, overwrite: bool = False) -> str:
@@ -98,8 +98,10 @@ def create_function_command_file(function_info: FunctionInfo, output_dir: str, f
98
98
  call_param = ""
99
99
  call_arg = ""
100
100
 
101
- # Add Output class
102
- command_file_content += f" class Output(BaseModel):\n{output_fields}\n\n"
101
+ # Add Output class with arbitrary_types_allowed for non-Pydantic return types
102
+ command_file_content += f" class Output(BaseModel):\n"
103
+ command_file_content += f" model_config = ConfigDict(arbitrary_types_allowed=True)\n"
104
+ command_file_content += f"{output_fields}\n\n"
103
105
 
104
106
  # Add utterances
105
107
  command_file_content += f" plain_utterances = [\n{plain_utterances}\n ]\n\n"
@@ -326,8 +328,10 @@ def create_command_file(class_info, method_info, output_dir, file_name=None, is_
326
328
  # Add Input class if needed
327
329
  command_file_content += input_class
328
330
 
329
- # Add Output class
330
- command_file_content += f" class Output(BaseModel):\n{output_fields}\n\n"
331
+ # Add Output class with arbitrary_types_allowed for non-Pydantic return types
332
+ command_file_content += f" class Output(BaseModel):\n"
333
+ command_file_content += f" model_config = ConfigDict(arbitrary_types_allowed=True)\n"
334
+ command_file_content += f"{output_fields}\n\n"
331
335
 
332
336
  # Add utterances
333
337
  command_file_content += f" plain_utterances = [\n{plain_utterances}\n ]\n\n"
@@ -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>
@@ -20,6 +20,7 @@ See docs/fastworkflow_fastapi_spec.md for complete specification.
20
20
 
21
21
  import asyncio
22
22
  import json
23
+ import logging
23
24
  import os
24
25
  import queue
25
26
  import time
@@ -28,15 +29,16 @@ from contextlib import asynccontextmanager
28
29
  import argparse
29
30
 
30
31
  import uvicorn
31
- from jose import JWTError
32
+ from jwt.exceptions import PyJWTError as JWTError
32
33
  from dotenv import dotenv_values
33
34
 
34
35
  import fastworkflow
35
36
  from fastworkflow.utils.logging import logger
36
37
 
37
- from fastapi import FastAPI, HTTPException, status, Depends, Header
38
+ from fastapi import FastAPI, HTTPException, status, Depends, Header, Request
38
39
  from fastapi.middleware.cors import CORSMiddleware
39
40
  from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
41
+ from starlette.middleware.base import BaseHTTPMiddleware
40
42
 
41
43
  from .mcp_specific import setup_mcp
42
44
  from .utils import (
@@ -76,6 +78,103 @@ from .conversation_store import (
76
78
  )
77
79
 
78
80
 
81
+ # ============================================================================
82
+ # Probe Logging Filter Middleware
83
+ # ============================================================================
84
+
85
+ # Paths that should not be logged unless they return non-200 status
86
+ PROBE_PATHS = {"/probes/healthz", "/probes/readyz"}
87
+
88
+
89
+ class ProbeLoggingFilterMiddleware(BaseHTTPMiddleware):
90
+ """
91
+ Middleware to suppress logging for Kubernetes probe endpoints.
92
+
93
+ Probe endpoints (/probes/healthz, /probes/readyz) are called frequently by
94
+ Kubernetes and would generate excessive logs. This middleware only logs
95
+ probe requests when they return a non-200 status code.
96
+ """
97
+
98
+ async def dispatch(self, request: Request, call_next):
99
+ response = await call_next(request)
100
+
101
+ # Only log probe endpoints if they return non-200 status
102
+ if request.url.path in PROBE_PATHS and response.status_code != 200:
103
+ logger.warning(
104
+ f"Probe {request.url.path} returned status {response.status_code}"
105
+ )
106
+
107
+ return response
108
+
109
+
110
+ class ProbeAccessLogFilter(logging.Filter):
111
+ """
112
+ Filter to suppress successful probe requests from uvicorn's access logger.
113
+
114
+ This prevents Kubernetes health check spam in access logs while preserving
115
+ access logs for all other endpoints. Failed probes (non-200) are still logged
116
+ via ProbeLoggingFilterMiddleware at WARNING level.
117
+ """
118
+
119
+ def filter(self, record: logging.LogRecord) -> bool:
120
+ message = record.getMessage()
121
+ # Suppress logs for successful probe requests (contain path and 200 status)
122
+ for path in PROBE_PATHS:
123
+ if f'"{path}' in message and '" 200' in message:
124
+ return False # Suppress this log
125
+ return True
126
+
127
+
128
+ # ============================================================================
129
+ # Readiness State Tracking
130
+ # ============================================================================
131
+
132
+ class ReadinessState:
133
+ """
134
+ Tracks the readiness state of the application.
135
+
136
+ The application is considered ready when set_ready(True) is called,
137
+ typically after successful initialization in the lifespan startup.
138
+
139
+ Additional debug attributes (is_initialized, workflow_path_valid) are
140
+ retained for production debugging but do not control readiness.
141
+ """
142
+
143
+ def __init__(self):
144
+ self._is_ready = False
145
+ # Debug attributes - do not control readiness, used for diagnostics
146
+ self._is_initialized = False
147
+ self._workflow_path_valid = False
148
+
149
+ def set_ready(self, value: bool = True):
150
+ """Set the main readiness state. Called after successful initialization."""
151
+ self._is_ready = value
152
+
153
+ def set_initialized(self, value: bool = True):
154
+ """Mark FastWorkflow as initialized (for debugging/diagnostics)."""
155
+ self._is_initialized = value
156
+
157
+ def set_workflow_path_valid(self, value: bool = True):
158
+ """Mark workflow path as validated (for debugging/diagnostics)."""
159
+ self._workflow_path_valid = value
160
+
161
+ def is_ready(self) -> bool:
162
+ """Check if the application is ready to serve traffic."""
163
+ return self._is_ready
164
+
165
+ def get_status(self) -> dict:
166
+ """Get detailed readiness status for debugging."""
167
+ return {
168
+ "ready": self._is_ready,
169
+ "fastworkflow_initialized": self._is_initialized,
170
+ "workflow_path_valid": self._workflow_path_valid
171
+ }
172
+
173
+
174
+ # Global readiness state
175
+ readiness_state = ReadinessState()
176
+
177
+
79
178
  # ============================================================================
80
179
  # Session Management
81
180
  # ============================================================================
@@ -149,8 +248,6 @@ async def get_session_and_ensure_runtime(
149
248
  @asynccontextmanager
150
249
  async def lifespan(_app: FastAPI):
151
250
  """Startup and shutdown hooks"""
152
- logger.info("FastWorkflow FastAPI service starting...")
153
- logger.info(f"Startup with CLI params: workflow_path={ARGS.workflow_path}, env_file_path={ARGS.env_file_path}, passwords_file_path={ARGS.passwords_file_path}")
154
251
 
155
252
  def initialize_fastworkflow_on_startup() -> None:
156
253
  env_vars: dict[str, str] = {}
@@ -162,6 +259,17 @@ async def lifespan(_app: FastAPI):
162
259
 
163
260
  # Configure JWT verification mode based on CLI parameter
164
261
  set_jwt_verification_mode(ARGS.expect_encrypted_jwt)
262
+
263
+ # Mark FastWorkflow as initialized for readiness probe
264
+ readiness_state.set_initialized(True)
265
+
266
+ # Validate workflow path for readiness probe
267
+ if ARGS.workflow_path and os.path.exists(ARGS.workflow_path):
268
+ readiness_state.set_workflow_path_valid(True)
269
+ logger.info(f"Workflow path validated: {ARGS.workflow_path}")
270
+ else:
271
+ logger.warning(f"Workflow path not valid or not found: {ARGS.workflow_path}")
272
+ readiness_state.set_workflow_path_valid(False)
165
273
 
166
274
  async def _active_turn_channel_ids() -> list[str]:
167
275
  active: list[str] = []
@@ -213,6 +321,12 @@ async def lifespan(_app: FastAPI):
213
321
 
214
322
  try:
215
323
  initialize_fastworkflow_on_startup()
324
+ # Log startup info AFTER init() so log level from env file is respected
325
+ logger.info("FastWorkflow FastAPI service starting...")
326
+ logger.info(f"Startup with CLI params: workflow_path={ARGS.workflow_path}, env_file_path={ARGS.env_file_path}, passwords_file_path={ARGS.passwords_file_path}")
327
+ # Mark application as ready to accept traffic
328
+ readiness_state.set_ready(True)
329
+ logger.info("Application ready to accept traffic")
216
330
  yield
217
331
  finally:
218
332
  logger.info("FastWorkflow FastAPI service shutting down...")
@@ -275,8 +389,8 @@ def custom_openapi():
275
389
 
276
390
  # Apply security globally to all endpoints except public ones
277
391
  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"]:
392
+ # Skip endpoints that don't require authentication (including probe endpoints)
393
+ if path in ["/initialize", "/refresh_token", "/", "/admin/dump_all_conversations", "/admin/generate_mcp_token", "/probes/healthz", "/probes/readyz"]:
280
394
  continue
281
395
  for method in path_item:
282
396
  if method in ["get", "post", "put", "delete", "patch"] and "security" not in path_item[method]:
@@ -296,6 +410,9 @@ app.add_middleware(
296
410
  allow_headers=["*"],
297
411
  )
298
412
 
413
+ # Probe logging filter middleware - suppresses logs for successful probe requests
414
+ app.add_middleware(ProbeLoggingFilterMiddleware)
415
+
299
416
  # ============================================================================
300
417
  # Endpoints
301
418
  # ============================================================================
@@ -317,6 +434,91 @@ async def root():
317
434
  """
318
435
 
319
436
 
437
+ # ============================================================================
438
+ # Kubernetes Probe Endpoints
439
+ # ============================================================================
440
+
441
+ @app.get(
442
+ "/probes/healthz",
443
+ operation_id="liveness_probe",
444
+ status_code=status.HTTP_200_OK,
445
+ responses={
446
+ 200: {"description": "Application is alive and running"},
447
+ 503: {"description": "Application is unhealthy"}
448
+ },
449
+ tags=["probes"]
450
+ )
451
+ async def liveness_probe() -> dict:
452
+ """
453
+ Liveness probe endpoint for Kubernetes.
454
+
455
+ Determines whether the container is still running. If this probe fails,
456
+ Kubernetes will restart the container.
457
+
458
+ This endpoint checks basic application health:
459
+ - The FastAPI application is responsive
460
+ - The event loop is processing requests
461
+
462
+ This endpoint is not logged unless it returns a non-200 status code
463
+ to avoid excessive logging from frequent Kubernetes health checks.
464
+
465
+ Returns:
466
+ 200 OK: {"status": "alive"} - Application is running normally
467
+ 503 Service Unavailable: Application is unhealthy
468
+ """
469
+ # Basic liveness check - if we can respond, we're alive
470
+ # The application is considered "live" if it can process HTTP requests
471
+ return {"status": "alive"}
472
+
473
+
474
+ @app.get(
475
+ "/probes/readyz",
476
+ operation_id="readiness_probe",
477
+ status_code=status.HTTP_200_OK,
478
+ responses={
479
+ 200: {"description": "Application is ready to accept traffic"},
480
+ 503: {"description": "Application is not ready to accept traffic"}
481
+ },
482
+ tags=["probes"]
483
+ )
484
+ async def readiness_probe() -> JSONResponse:
485
+ """
486
+ Readiness probe endpoint for Kubernetes.
487
+
488
+ Checks whether the container is ready to accept traffic. Kubernetes only
489
+ routes traffic to containers that pass the readiness check.
490
+
491
+ This endpoint verifies:
492
+ - FastWorkflow has been initialized
493
+ - The configured workflow path is valid and accessible
494
+
495
+ This endpoint is not logged unless it returns a non-200 status code
496
+ to avoid excessive logging from frequent Kubernetes health checks.
497
+
498
+ Returns:
499
+ 200 OK: {"status": "ready", "checks": {...}} - Ready to accept traffic
500
+ 503 Service Unavailable: {"status": "not_ready", "checks": {...}} - Not ready
501
+ """
502
+ status_info = readiness_state.get_status()
503
+
504
+ if readiness_state.is_ready():
505
+ return JSONResponse(
506
+ status_code=status.HTTP_200_OK,
507
+ content={
508
+ "status": "ready",
509
+ "checks": status_info
510
+ }
511
+ )
512
+ else:
513
+ return JSONResponse(
514
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
515
+ content={
516
+ "status": "not_ready",
517
+ "checks": status_info
518
+ }
519
+ )
520
+
521
+
320
522
  @app.post(
321
523
  "/initialize",
322
524
  operation_id="rest_initialize",
@@ -1303,6 +1505,12 @@ def main():
1303
1505
  """Entry point for the FastAPI MCP server."""
1304
1506
  host = ARGS.host if hasattr(ARGS, 'host') else "0.0.0.0"
1305
1507
  port = ARGS.port if hasattr(ARGS, 'port') else 8000
1508
+
1509
+ # Add filter to suppress successful probe requests from uvicorn's access logger
1510
+ # This preserves access logs for other endpoints while eliminating probe spam
1511
+ # Probe failures (non-200) are still logged via ProbeLoggingFilterMiddleware at WARNING level
1512
+ logging.getLogger("uvicorn.access").addFilter(ProbeAccessLogFilter())
1513
+
1306
1514
  uvicorn.run(app, host=host, port=port)
1307
1515
 
1308
1516
  if __name__ == "__main__":
@@ -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
@@ -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)
@@ -113,6 +113,48 @@ logging.getLogger("speedict").setLevel(logging.WARNING)
113
113
  logging.getLogger("filelock").setLevel(logging.WARNING)
114
114
  logging.getLogger("datasets").setLevel(logging.WARNING)
115
115
 
116
+
117
+ def reconfigure_log_level(log_level_str: str) -> None:
118
+ """Reconfigure the fastWorkflow logger's level after initialization.
119
+
120
+ This allows LOG_LEVEL to be set via dotenv files loaded after module import.
121
+
122
+ Args:
123
+ log_level_str: One of DEBUG, INFO, WARNING, ERROR, CRITICAL
124
+ """
125
+ global LOG_LEVEL
126
+
127
+ level_map = {
128
+ "DEBUG": logging.DEBUG,
129
+ "INFO": logging.INFO,
130
+ "WARNING": logging.WARNING,
131
+ "ERROR": logging.ERROR,
132
+ "CRITICAL": logging.CRITICAL,
133
+ }
134
+
135
+ log_level_str_upper = log_level_str.upper()
136
+ if log_level_str_upper not in level_map:
137
+ logger.warning(
138
+ f"Invalid LOG_LEVEL '{log_level_str}', must be one of {list(level_map.keys())}. Keeping current level."
139
+ )
140
+ return
141
+
142
+ new_level = level_map[log_level_str_upper]
143
+
144
+ # Only reconfigure if the level is actually different
145
+ if LOG_LEVEL == new_level:
146
+ return
147
+
148
+ LOG_LEVEL = new_level
149
+ logger.setLevel(LOG_LEVEL)
150
+
151
+ # Update all handlers
152
+ for handler in logger.handlers:
153
+ handler.setLevel(LOG_LEVEL)
154
+
155
+ logger.info(f"Log level reconfigured to {log_level_str_upper}")
156
+
157
+
116
158
  # some testing code
117
159
  if __name__ == "__main__":
118
160
  logger.debug("debug message")
@@ -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.26
3
+ Version: 2.17.28
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
@@ -126,6 +133,9 @@ While [DSPy](https://dspy.ai) ([Why DSPy](https://x.com/lateinteraction/status/1
126
133
  - [Adding context hierarchies with context_inheritance_model.json](#adding-context-hierarchies-with-context_inheritance_modeljson)
127
134
  - [Using DSPy for Response Generation](#using-dspy-for-response-generation)
128
135
  - [Using Startup Commands and Actions](#using-startup-commands-and-actions)
136
+ - [Running FastWorkflow as a FastAPI Service](#running-fastworkflow-as-a-fastapi-service)
137
+ - [Kubernetes Liveness and Readiness Probes](#kubernetes-liveness-and-readiness-probes)
138
+ - [Using LiteLLM Proxy](#using-litellm-proxy)
129
139
  - [Rapidly Building Workflows with the Build Tool](#rapidly-building-workflows-with-the-build-tool)
130
140
  - [Environment Variables Reference](#environment-variables-reference)
131
141
  - [Environment Variables](#environment-variables)
@@ -575,6 +585,110 @@ For workflows with complex initialization requirements, creating a dedicated sta
575
585
 
576
586
  ---
577
587
 
588
+ ### Running FastWorkflow as a FastAPI Service
589
+
590
+ For production deployments and integrations, fastWorkflow provides a FastAPI-based HTTP service via the `run_fastapi_mcp` module. This exposes your workflow as REST endpoints with JWT authentication, SSE streaming, and MCP (Model Context Protocol) support.
591
+
592
+ ```sh
593
+ # Run the FastAPI service
594
+ python -m fastworkflow.run_fastapi_mcp \
595
+ --workflow_path ./my_workflow \
596
+ --env_file_path ./fastworkflow.env \
597
+ --passwords_file_path ./fastworkflow.passwords.env \
598
+ --port 8000
599
+ ```
600
+
601
+ The service provides endpoints for:
602
+ - `/initialize` - Create a new session and obtain JWT tokens
603
+ - `/invoke_agent` - Submit natural language queries to the agent
604
+ - `/invoke_agent_stream` - Stream responses via SSE or NDJSON
605
+ - `/perform_action` - Execute workflow actions directly
606
+ - `/conversations` - Manage conversation history
607
+
608
+ #### Kubernetes Liveness and Readiness Probes
609
+
610
+ When deploying fastWorkflow in Kubernetes, the service provides dedicated probe endpoints for health monitoring:
611
+
612
+ **Liveness Probe (`/probes/healthz`)**
613
+
614
+ Determines whether the container is still running. If this probe fails, Kubernetes will restart the container.
615
+
616
+ ```sh
617
+ curl http://localhost:8000/probes/healthz
618
+ # Response: {"status": "alive"}
619
+ ```
620
+
621
+ **Readiness Probe (`/probes/readyz`)**
622
+
623
+ Checks whether the container is ready to accept traffic. Kubernetes only routes traffic to containers that pass this check.
624
+
625
+ ```sh
626
+ curl http://localhost:8000/probes/readyz
627
+ # Response (ready): {"status": "ready", "checks": {"ready": true, "fastworkflow_initialized": true, "workflow_path_valid": true}}
628
+ # Response (not ready): {"status": "not_ready", "checks": {"ready": false, ...}}
629
+ ```
630
+
631
+ The readiness probe returns:
632
+ - `200 OK` when the application is ready to serve traffic
633
+ - `503 Service Unavailable` when not ready (e.g., during startup)
634
+
635
+ **Example Kubernetes Configuration**
636
+
637
+ ```yaml
638
+ apiVersion: apps/v1
639
+ kind: Deployment
640
+ spec:
641
+ template:
642
+ spec:
643
+ containers:
644
+ - name: fastworkflow
645
+ livenessProbe:
646
+ httpGet:
647
+ path: /probes/healthz
648
+ port: 8000
649
+ initialDelaySeconds: 10
650
+ periodSeconds: 10
651
+ failureThreshold: 5
652
+ timeoutSeconds: 3
653
+ readinessProbe:
654
+ httpGet:
655
+ path: /probes/readyz
656
+ port: 8000
657
+ initialDelaySeconds: 5
658
+ periodSeconds: 5
659
+ failureThreshold: 3
660
+ timeoutSeconds: 3
661
+ ```
662
+
663
+ > [!note]
664
+ > Probe endpoints do not require authentication and are excluded from request logging when returning 200 status to avoid excessive log noise from frequent Kubernetes health checks.
665
+
666
+ #### Using LiteLLM Proxy
667
+
668
+ FastWorkflow supports routing LLM calls through a [LiteLLM Proxy](https://docs.litellm.ai/docs/simple_proxy) server. This is useful when you want to:
669
+ - Centralize API key management
670
+ - Use a unified endpoint for multiple LLM providers
671
+ - Route requests through a corporate proxy with custom configurations
672
+
673
+ To use LiteLLM Proxy, set your model strings to use the `litellm_proxy/` prefix and configure the proxy URL:
674
+
675
+ ```
676
+ # In fastworkflow.env - use the litellm_proxy/ prefix for model names
677
+ LLM_AGENT=litellm_proxy/bedrock_mistral_large_2407
678
+ LLM_PARAM_EXTRACTION=litellm_proxy/bedrock_mistral_large_2407
679
+ LITELLM_PROXY_API_BASE=http://127.0.0.1:4000
680
+
681
+ # In fastworkflow.passwords.env - shared key for proxy authentication
682
+ LITELLM_PROXY_API_KEY=your-proxy-api-key
683
+ ```
684
+
685
+ 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.
686
+
687
+ > [!note]
688
+ > 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.
689
+
690
+ ---
691
+
578
692
  ## Rapidly Building Workflows with the Build Tool
579
693
 
580
694
  After understanding the manual process, you can use the `fastworkflow build` command to automate everything. It introspects your code and generates all the necessary files.
@@ -606,6 +720,7 @@ This single command will generate the `greet.py` command, `get_properties` and `
606
720
  | `LLM_PLANNER` | LiteLLM model string for the agent's task planner | `run` (agent mode) | `mistral/mistral-small-latest` |
607
721
  | `LLM_AGENT` | LiteLLM model string for the DSPy agent | `run` (agent mode) | `mistral/mistral-small-latest` |
608
722
  | `LLM_CONVERSATION_STORE` | LiteLLM model string for conversation topic/summary generation | FastAPI service | `mistral/mistral-small-latest` |
723
+ | `LITELLM_PROXY_API_BASE` | URL of your LiteLLM Proxy server | When using `litellm_proxy/` models | *not set* |
609
724
  | `NOT_FOUND` | Placeholder value for missing parameters during extraction | Always | `"NOT_FOUND"` |
610
725
  | `MISSING_INFORMATION_ERRMSG` | Error message prefix for missing parameters | Always | `"Missing required..."` |
611
726
  | `INVALID_INFORMATION_ERRMSG` | Error message prefix for invalid parameters | Always | `"Invalid information..."` |
@@ -620,6 +735,7 @@ This single command will generate the `greet.py` command, `get_properties` and `
620
735
  | `LITELLM_API_KEY_PLANNER`| API key for the `LLM_PLANNER` model | `run` (agent mode) | *required* |
621
736
  | `LITELLM_API_KEY_AGENT`| API key for the `LLM_AGENT` model | `run` (agent mode) | *required* |
622
737
  | `LITELLM_API_KEY_CONVERSATION_STORE`| API key for the `LLM_CONVERSATION_STORE` model | FastAPI service | *required* |
738
+ | `LITELLM_PROXY_API_KEY`| Shared API key for authenticating with LiteLLM Proxy | When using `litellm_proxy/` models | *optional* |
623
739
 
624
740
  > [!tip]
625
741
  > 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.
@@ -1,5 +1,5 @@
1
1
  fastworkflow/.DS_Store,sha256=CTvh7SVnPHlYsgd1jwLq9digT-k8QV6JS7JgXEw0OVo,8196
2
- fastworkflow/__init__.py,sha256=wrifGmkwLnHs6BupEliX20cDuV7WkQEKjNspunkzBlc,7604
2
+ fastworkflow/__init__.py,sha256=SiOTpsqqyHkg4XTeh9jveBPEn93rKdQG3xLTnBjskfU,7922
3
3
  fastworkflow/_commands/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  fastworkflow/_workflows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  fastworkflow/_workflows/command_metadata_extraction/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -20,7 +20,7 @@ fastworkflow/build/class_analysis_structures.py,sha256=UWOKcs9pCiNuXc64hNywkTJq5
20
20
  fastworkflow/build/cli_specification.md,sha256=zAVG6wv7AzSotimAQlQmuq6y6ZumhdtftUBcKw3NdA0,3742
21
21
  fastworkflow/build/command_dependency_resolver.py,sha256=nNE7nANeDPXLvBlKkIS-8XVkNbg5dPVXB4lJMMANju8,3211
22
22
  fastworkflow/build/command_file_generator.py,sha256=i9ZcQLFUr-m1pxK1Vo79ons3yBzniuytx5vvY-0QfZ8,19630
23
- fastworkflow/build/command_file_template.py,sha256=U1t36YVtDUZVgBuXYsCw8ovK1ZQZkFhWYqNHvoI3hwk,18948
23
+ fastworkflow/build/command_file_template.py,sha256=MmKFnj81SDiNP8h2B4Pec_15WLWYmkR2radVVqfl_Go,19336
24
24
  fastworkflow/build/command_import_utils.py,sha256=nwyiurA9HDTLig-e9crBYVsVjkj4qfnLl8CqHUgs9XM,2127
25
25
  fastworkflow/build/command_stub_generator.py,sha256=KFOkAZfHeqyDlv59A3PfbuLn7jPXxXp-6MbjWO25LqE,13989
26
26
  fastworkflow/build/context_folder_generator.py,sha256=VpKoz3KYKGzCEFPqdVSwGLXLnrqN4MBU7TusOAhzfr0,5254
@@ -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,12 +148,12 @@ 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=N-5dWluDFF0fai0xUICwIE2VQWa2Zvu_iERiDmh1Aag,61701
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
@@ -163,11 +163,11 @@ fastworkflow/utils/chat_adapter.py,sha256=NVIvSmd0L4QNYTg2oEZzR1iCdPjti0C0ZMffay
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=Wj0kj6Cfdh1Ftig0NnVWJl-YxB7lxUEj1T_AcJPuEiQ,5304
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
@@ -175,10 +175,10 @@ fastworkflow/utils/react.py,sha256=dmDn0huU_rp6z4p-gKwag5Btlmcb9ZsnukO1tXNFTGQ,1
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.26.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
181
- fastworkflow-2.17.26.dist-info/METADATA,sha256=-rCeYKc7fwI4iAygy7sUGQDUukm-RoExYVtHcBLyM8I,31418
182
- fastworkflow-2.17.26.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
183
- fastworkflow-2.17.26.dist-info/entry_points.txt,sha256=m8HqoPzCyaZLAx-V5X8MJgw3Lx3GiPDlxNEZ7K-Gb-U,54
184
- fastworkflow-2.17.26.dist-info/RECORD,,
180
+ fastworkflow-2.17.28.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
181
+ fastworkflow-2.17.28.dist-info/METADATA,sha256=bdD5T06K4bb2IENo_dAYdcZlc5pzlH2riuLFy8BkwBg,36038
182
+ fastworkflow-2.17.28.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
183
+ fastworkflow-2.17.28.dist-info/entry_points.txt,sha256=m8HqoPzCyaZLAx-V5X8MJgw3Lx3GiPDlxNEZ7K-Gb-U,54
184
+ fastworkflow-2.17.28.dist-info/RECORD,,