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 +6 -0
- fastworkflow/build/command_file_template.py +9 -5
- fastworkflow/examples/fastworkflow.env +19 -0
- fastworkflow/examples/fastworkflow.passwords.env +14 -1
- fastworkflow/run_fastapi_mcp/__main__.py +214 -6
- fastworkflow/run_fastapi_mcp/jwt_manager.py +5 -5
- fastworkflow/run_fastapi_mcp/utils.py +1 -1
- fastworkflow/utils/dspy_utils.py +56 -2
- fastworkflow/utils/logging.py +44 -2
- fastworkflow/workflow_agent.py +12 -13
- {fastworkflow-2.17.26.dist-info → fastworkflow-2.17.28.dist-info}/METADATA +119 -3
- {fastworkflow-2.17.26.dist-info → fastworkflow-2.17.28.dist-info}/RECORD +15 -15
- {fastworkflow-2.17.26.dist-info → fastworkflow-2.17.28.dist-info}/LICENSE +0 -0
- {fastworkflow-2.17.26.dist-info → fastworkflow-2.17.28.dist-info}/WHEEL +0 -0
- {fastworkflow-2.17.26.dist-info → fastworkflow-2.17.28.dist-info}/entry_points.txt +0 -0
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
|
|
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
|
|
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
|
|
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
|
-
|
|
13
|
-
from
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
|
10
|
+
from jwt.exceptions import PyJWTError as JWTError
|
|
11
11
|
from pydantic import BaseModel, field_validator
|
|
12
12
|
|
|
13
13
|
import fastworkflow
|
fastworkflow/utils/dspy_utils.py
CHANGED
|
@@ -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
|
-
"""
|
|
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
|
|
fastworkflow/utils/logging.py
CHANGED
|
@@ -47,7 +47,7 @@ class FormatterNs(logging.Formatter):
|
|
|
47
47
|
|
|
48
48
|
logging.setLogRecordFactory(LogRecordNs)
|
|
49
49
|
|
|
50
|
-
LOG_FORMAT = "%(
|
|
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("%(
|
|
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")
|
fastworkflow/workflow_agent.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
366
|
-
user_query_and_next_steps = f"{user_query}\n\nExecute these next steps:\n{
|
|
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.
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
54
|
-
fastworkflow/examples/fastworkflow.passwords.env,sha256=
|
|
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=
|
|
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=
|
|
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
|
|
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=
|
|
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=
|
|
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=
|
|
178
|
+
fastworkflow/workflow_agent.py,sha256=jCvMyz5mLr8UX5QN1ssWebs4f24XhirjGkoJpsS-qZ0,19202
|
|
179
179
|
fastworkflow/workflow_inheritance_model.py,sha256=Pp-qSrQISgPfPjJVUfW84pc7HLmL2evuq0UVIYR51K0,7974
|
|
180
|
-
fastworkflow-2.17.
|
|
181
|
-
fastworkflow-2.17.
|
|
182
|
-
fastworkflow-2.17.
|
|
183
|
-
fastworkflow-2.17.
|
|
184
|
-
fastworkflow-2.17.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|