PraisonAI 3.0.0__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.
- praisonai/__init__.py +54 -0
- praisonai/__main__.py +15 -0
- praisonai/acp/__init__.py +54 -0
- praisonai/acp/config.py +159 -0
- praisonai/acp/server.py +587 -0
- praisonai/acp/session.py +219 -0
- praisonai/adapters/__init__.py +50 -0
- praisonai/adapters/readers.py +395 -0
- praisonai/adapters/rerankers.py +315 -0
- praisonai/adapters/retrievers.py +394 -0
- praisonai/adapters/vector_stores.py +409 -0
- praisonai/agent_scheduler.py +337 -0
- praisonai/agents_generator.py +903 -0
- praisonai/api/call.py +292 -0
- praisonai/auto.py +1197 -0
- praisonai/capabilities/__init__.py +275 -0
- praisonai/capabilities/a2a.py +140 -0
- praisonai/capabilities/assistants.py +283 -0
- praisonai/capabilities/audio.py +320 -0
- praisonai/capabilities/batches.py +469 -0
- praisonai/capabilities/completions.py +336 -0
- praisonai/capabilities/container_files.py +155 -0
- praisonai/capabilities/containers.py +93 -0
- praisonai/capabilities/embeddings.py +158 -0
- praisonai/capabilities/files.py +467 -0
- praisonai/capabilities/fine_tuning.py +293 -0
- praisonai/capabilities/guardrails.py +182 -0
- praisonai/capabilities/images.py +330 -0
- praisonai/capabilities/mcp.py +190 -0
- praisonai/capabilities/messages.py +270 -0
- praisonai/capabilities/moderations.py +154 -0
- praisonai/capabilities/ocr.py +217 -0
- praisonai/capabilities/passthrough.py +204 -0
- praisonai/capabilities/rag.py +207 -0
- praisonai/capabilities/realtime.py +160 -0
- praisonai/capabilities/rerank.py +165 -0
- praisonai/capabilities/responses.py +266 -0
- praisonai/capabilities/search.py +109 -0
- praisonai/capabilities/skills.py +133 -0
- praisonai/capabilities/vector_store_files.py +334 -0
- praisonai/capabilities/vector_stores.py +304 -0
- praisonai/capabilities/videos.py +141 -0
- praisonai/chainlit_ui.py +304 -0
- praisonai/chat/__init__.py +106 -0
- praisonai/chat/app.py +125 -0
- praisonai/cli/__init__.py +26 -0
- praisonai/cli/app.py +213 -0
- praisonai/cli/commands/__init__.py +75 -0
- praisonai/cli/commands/acp.py +70 -0
- praisonai/cli/commands/completion.py +333 -0
- praisonai/cli/commands/config.py +166 -0
- praisonai/cli/commands/debug.py +142 -0
- praisonai/cli/commands/diag.py +55 -0
- praisonai/cli/commands/doctor.py +166 -0
- praisonai/cli/commands/environment.py +179 -0
- praisonai/cli/commands/lsp.py +112 -0
- praisonai/cli/commands/mcp.py +210 -0
- praisonai/cli/commands/profile.py +457 -0
- praisonai/cli/commands/run.py +228 -0
- praisonai/cli/commands/schedule.py +150 -0
- praisonai/cli/commands/serve.py +97 -0
- praisonai/cli/commands/session.py +212 -0
- praisonai/cli/commands/traces.py +145 -0
- praisonai/cli/commands/version.py +101 -0
- praisonai/cli/configuration/__init__.py +18 -0
- praisonai/cli/configuration/loader.py +353 -0
- praisonai/cli/configuration/paths.py +114 -0
- praisonai/cli/configuration/schema.py +164 -0
- praisonai/cli/features/__init__.py +268 -0
- praisonai/cli/features/acp.py +236 -0
- praisonai/cli/features/action_orchestrator.py +546 -0
- praisonai/cli/features/agent_scheduler.py +773 -0
- praisonai/cli/features/agent_tools.py +474 -0
- praisonai/cli/features/agents.py +375 -0
- praisonai/cli/features/at_mentions.py +471 -0
- praisonai/cli/features/auto_memory.py +182 -0
- praisonai/cli/features/autonomy_mode.py +490 -0
- praisonai/cli/features/background.py +356 -0
- praisonai/cli/features/base.py +168 -0
- praisonai/cli/features/capabilities.py +1326 -0
- praisonai/cli/features/checkpoints.py +338 -0
- praisonai/cli/features/code_intelligence.py +652 -0
- praisonai/cli/features/compaction.py +294 -0
- praisonai/cli/features/compare.py +534 -0
- praisonai/cli/features/cost_tracker.py +514 -0
- praisonai/cli/features/debug.py +810 -0
- praisonai/cli/features/deploy.py +517 -0
- praisonai/cli/features/diag.py +289 -0
- praisonai/cli/features/doctor/__init__.py +63 -0
- praisonai/cli/features/doctor/checks/__init__.py +24 -0
- praisonai/cli/features/doctor/checks/acp_checks.py +240 -0
- praisonai/cli/features/doctor/checks/config_checks.py +366 -0
- praisonai/cli/features/doctor/checks/db_checks.py +366 -0
- praisonai/cli/features/doctor/checks/env_checks.py +543 -0
- praisonai/cli/features/doctor/checks/lsp_checks.py +199 -0
- praisonai/cli/features/doctor/checks/mcp_checks.py +349 -0
- praisonai/cli/features/doctor/checks/memory_checks.py +268 -0
- praisonai/cli/features/doctor/checks/network_checks.py +251 -0
- praisonai/cli/features/doctor/checks/obs_checks.py +328 -0
- praisonai/cli/features/doctor/checks/performance_checks.py +235 -0
- praisonai/cli/features/doctor/checks/permissions_checks.py +259 -0
- praisonai/cli/features/doctor/checks/selftest_checks.py +322 -0
- praisonai/cli/features/doctor/checks/serve_checks.py +426 -0
- praisonai/cli/features/doctor/checks/skills_checks.py +231 -0
- praisonai/cli/features/doctor/checks/tools_checks.py +371 -0
- praisonai/cli/features/doctor/engine.py +266 -0
- praisonai/cli/features/doctor/formatters.py +310 -0
- praisonai/cli/features/doctor/handler.py +397 -0
- praisonai/cli/features/doctor/models.py +264 -0
- praisonai/cli/features/doctor/registry.py +239 -0
- praisonai/cli/features/endpoints.py +1019 -0
- praisonai/cli/features/eval.py +560 -0
- praisonai/cli/features/external_agents.py +231 -0
- praisonai/cli/features/fast_context.py +410 -0
- praisonai/cli/features/flow_display.py +566 -0
- praisonai/cli/features/git_integration.py +651 -0
- praisonai/cli/features/guardrail.py +171 -0
- praisonai/cli/features/handoff.py +185 -0
- praisonai/cli/features/hooks.py +583 -0
- praisonai/cli/features/image.py +384 -0
- praisonai/cli/features/interactive_runtime.py +585 -0
- praisonai/cli/features/interactive_tools.py +380 -0
- praisonai/cli/features/interactive_tui.py +603 -0
- praisonai/cli/features/jobs.py +632 -0
- praisonai/cli/features/knowledge.py +531 -0
- praisonai/cli/features/lite.py +244 -0
- praisonai/cli/features/lsp_cli.py +225 -0
- praisonai/cli/features/mcp.py +169 -0
- praisonai/cli/features/message_queue.py +587 -0
- praisonai/cli/features/metrics.py +211 -0
- praisonai/cli/features/n8n.py +673 -0
- praisonai/cli/features/observability.py +293 -0
- praisonai/cli/features/ollama.py +361 -0
- praisonai/cli/features/output_style.py +273 -0
- praisonai/cli/features/package.py +631 -0
- praisonai/cli/features/performance.py +308 -0
- praisonai/cli/features/persistence.py +636 -0
- praisonai/cli/features/profile.py +226 -0
- praisonai/cli/features/profiler/__init__.py +81 -0
- praisonai/cli/features/profiler/core.py +558 -0
- praisonai/cli/features/profiler/optimizations.py +652 -0
- praisonai/cli/features/profiler/suite.py +386 -0
- praisonai/cli/features/profiling.py +350 -0
- praisonai/cli/features/queue/__init__.py +73 -0
- praisonai/cli/features/queue/manager.py +395 -0
- praisonai/cli/features/queue/models.py +286 -0
- praisonai/cli/features/queue/persistence.py +564 -0
- praisonai/cli/features/queue/scheduler.py +484 -0
- praisonai/cli/features/queue/worker.py +372 -0
- praisonai/cli/features/recipe.py +1723 -0
- praisonai/cli/features/recipes.py +449 -0
- praisonai/cli/features/registry.py +229 -0
- praisonai/cli/features/repo_map.py +860 -0
- praisonai/cli/features/router.py +466 -0
- praisonai/cli/features/sandbox_executor.py +515 -0
- praisonai/cli/features/serve.py +829 -0
- praisonai/cli/features/session.py +222 -0
- praisonai/cli/features/skills.py +856 -0
- praisonai/cli/features/slash_commands.py +650 -0
- praisonai/cli/features/telemetry.py +179 -0
- praisonai/cli/features/templates.py +1384 -0
- praisonai/cli/features/thinking.py +305 -0
- praisonai/cli/features/todo.py +334 -0
- praisonai/cli/features/tools.py +680 -0
- praisonai/cli/features/tui/__init__.py +83 -0
- praisonai/cli/features/tui/app.py +580 -0
- praisonai/cli/features/tui/cli.py +566 -0
- praisonai/cli/features/tui/debug.py +511 -0
- praisonai/cli/features/tui/events.py +99 -0
- praisonai/cli/features/tui/mock_provider.py +328 -0
- praisonai/cli/features/tui/orchestrator.py +652 -0
- praisonai/cli/features/tui/screens/__init__.py +50 -0
- praisonai/cli/features/tui/screens/main.py +245 -0
- praisonai/cli/features/tui/screens/queue.py +174 -0
- praisonai/cli/features/tui/screens/session.py +124 -0
- praisonai/cli/features/tui/screens/settings.py +148 -0
- praisonai/cli/features/tui/widgets/__init__.py +56 -0
- praisonai/cli/features/tui/widgets/chat.py +261 -0
- praisonai/cli/features/tui/widgets/composer.py +224 -0
- praisonai/cli/features/tui/widgets/queue_panel.py +200 -0
- praisonai/cli/features/tui/widgets/status.py +167 -0
- praisonai/cli/features/tui/widgets/tool_panel.py +248 -0
- praisonai/cli/features/workflow.py +720 -0
- praisonai/cli/legacy.py +236 -0
- praisonai/cli/main.py +5559 -0
- praisonai/cli/schedule_cli.py +54 -0
- praisonai/cli/state/__init__.py +31 -0
- praisonai/cli/state/identifiers.py +161 -0
- praisonai/cli/state/sessions.py +313 -0
- praisonai/code/__init__.py +93 -0
- praisonai/code/agent_tools.py +344 -0
- praisonai/code/diff/__init__.py +21 -0
- praisonai/code/diff/diff_strategy.py +432 -0
- praisonai/code/tools/__init__.py +27 -0
- praisonai/code/tools/apply_diff.py +221 -0
- praisonai/code/tools/execute_command.py +275 -0
- praisonai/code/tools/list_files.py +274 -0
- praisonai/code/tools/read_file.py +206 -0
- praisonai/code/tools/search_replace.py +248 -0
- praisonai/code/tools/write_file.py +217 -0
- praisonai/code/utils/__init__.py +46 -0
- praisonai/code/utils/file_utils.py +307 -0
- praisonai/code/utils/ignore_utils.py +308 -0
- praisonai/code/utils/text_utils.py +276 -0
- praisonai/db/__init__.py +64 -0
- praisonai/db/adapter.py +531 -0
- praisonai/deploy/__init__.py +62 -0
- praisonai/deploy/api.py +231 -0
- praisonai/deploy/docker.py +454 -0
- praisonai/deploy/doctor.py +367 -0
- praisonai/deploy/main.py +327 -0
- praisonai/deploy/models.py +179 -0
- praisonai/deploy/providers/__init__.py +33 -0
- praisonai/deploy/providers/aws.py +331 -0
- praisonai/deploy/providers/azure.py +358 -0
- praisonai/deploy/providers/base.py +101 -0
- praisonai/deploy/providers/gcp.py +314 -0
- praisonai/deploy/schema.py +208 -0
- praisonai/deploy.py +185 -0
- praisonai/endpoints/__init__.py +53 -0
- praisonai/endpoints/a2u_server.py +410 -0
- praisonai/endpoints/discovery.py +165 -0
- praisonai/endpoints/providers/__init__.py +28 -0
- praisonai/endpoints/providers/a2a.py +253 -0
- praisonai/endpoints/providers/a2u.py +208 -0
- praisonai/endpoints/providers/agents_api.py +171 -0
- praisonai/endpoints/providers/base.py +231 -0
- praisonai/endpoints/providers/mcp.py +263 -0
- praisonai/endpoints/providers/recipe.py +206 -0
- praisonai/endpoints/providers/tools_mcp.py +150 -0
- praisonai/endpoints/registry.py +131 -0
- praisonai/endpoints/server.py +161 -0
- praisonai/inbuilt_tools/__init__.py +24 -0
- praisonai/inbuilt_tools/autogen_tools.py +117 -0
- praisonai/inc/__init__.py +2 -0
- praisonai/inc/config.py +96 -0
- praisonai/inc/models.py +155 -0
- praisonai/integrations/__init__.py +56 -0
- praisonai/integrations/base.py +303 -0
- praisonai/integrations/claude_code.py +270 -0
- praisonai/integrations/codex_cli.py +255 -0
- praisonai/integrations/cursor_cli.py +195 -0
- praisonai/integrations/gemini_cli.py +222 -0
- praisonai/jobs/__init__.py +67 -0
- praisonai/jobs/executor.py +425 -0
- praisonai/jobs/models.py +230 -0
- praisonai/jobs/router.py +314 -0
- praisonai/jobs/server.py +186 -0
- praisonai/jobs/store.py +203 -0
- praisonai/llm/__init__.py +66 -0
- praisonai/llm/registry.py +382 -0
- praisonai/mcp_server/__init__.py +152 -0
- praisonai/mcp_server/adapters/__init__.py +74 -0
- praisonai/mcp_server/adapters/agents.py +128 -0
- praisonai/mcp_server/adapters/capabilities.py +168 -0
- praisonai/mcp_server/adapters/cli_tools.py +568 -0
- praisonai/mcp_server/adapters/extended_capabilities.py +462 -0
- praisonai/mcp_server/adapters/knowledge.py +93 -0
- praisonai/mcp_server/adapters/memory.py +104 -0
- praisonai/mcp_server/adapters/prompts.py +306 -0
- praisonai/mcp_server/adapters/resources.py +124 -0
- praisonai/mcp_server/adapters/tools_bridge.py +280 -0
- praisonai/mcp_server/auth/__init__.py +48 -0
- praisonai/mcp_server/auth/api_key.py +291 -0
- praisonai/mcp_server/auth/oauth.py +460 -0
- praisonai/mcp_server/auth/oidc.py +289 -0
- praisonai/mcp_server/auth/scopes.py +260 -0
- praisonai/mcp_server/cli.py +852 -0
- praisonai/mcp_server/elicitation.py +445 -0
- praisonai/mcp_server/icons.py +302 -0
- praisonai/mcp_server/recipe_adapter.py +573 -0
- praisonai/mcp_server/recipe_cli.py +824 -0
- praisonai/mcp_server/registry.py +703 -0
- praisonai/mcp_server/sampling.py +422 -0
- praisonai/mcp_server/server.py +490 -0
- praisonai/mcp_server/tasks.py +443 -0
- praisonai/mcp_server/transports/__init__.py +18 -0
- praisonai/mcp_server/transports/http_stream.py +376 -0
- praisonai/mcp_server/transports/stdio.py +132 -0
- praisonai/persistence/__init__.py +84 -0
- praisonai/persistence/config.py +238 -0
- praisonai/persistence/conversation/__init__.py +25 -0
- praisonai/persistence/conversation/async_mysql.py +427 -0
- praisonai/persistence/conversation/async_postgres.py +410 -0
- praisonai/persistence/conversation/async_sqlite.py +371 -0
- praisonai/persistence/conversation/base.py +151 -0
- praisonai/persistence/conversation/json_store.py +250 -0
- praisonai/persistence/conversation/mysql.py +387 -0
- praisonai/persistence/conversation/postgres.py +401 -0
- praisonai/persistence/conversation/singlestore.py +240 -0
- praisonai/persistence/conversation/sqlite.py +341 -0
- praisonai/persistence/conversation/supabase.py +203 -0
- praisonai/persistence/conversation/surrealdb.py +287 -0
- praisonai/persistence/factory.py +301 -0
- praisonai/persistence/hooks/__init__.py +18 -0
- praisonai/persistence/hooks/agent_hooks.py +297 -0
- praisonai/persistence/knowledge/__init__.py +26 -0
- praisonai/persistence/knowledge/base.py +144 -0
- praisonai/persistence/knowledge/cassandra.py +232 -0
- praisonai/persistence/knowledge/chroma.py +295 -0
- praisonai/persistence/knowledge/clickhouse.py +242 -0
- praisonai/persistence/knowledge/cosmosdb_vector.py +438 -0
- praisonai/persistence/knowledge/couchbase.py +286 -0
- praisonai/persistence/knowledge/lancedb.py +216 -0
- praisonai/persistence/knowledge/langchain_adapter.py +291 -0
- praisonai/persistence/knowledge/lightrag_adapter.py +212 -0
- praisonai/persistence/knowledge/llamaindex_adapter.py +256 -0
- praisonai/persistence/knowledge/milvus.py +277 -0
- praisonai/persistence/knowledge/mongodb_vector.py +306 -0
- praisonai/persistence/knowledge/pgvector.py +335 -0
- praisonai/persistence/knowledge/pinecone.py +253 -0
- praisonai/persistence/knowledge/qdrant.py +301 -0
- praisonai/persistence/knowledge/redis_vector.py +291 -0
- praisonai/persistence/knowledge/singlestore_vector.py +299 -0
- praisonai/persistence/knowledge/surrealdb_vector.py +309 -0
- praisonai/persistence/knowledge/upstash_vector.py +266 -0
- praisonai/persistence/knowledge/weaviate.py +223 -0
- praisonai/persistence/migrations/__init__.py +10 -0
- praisonai/persistence/migrations/manager.py +251 -0
- praisonai/persistence/orchestrator.py +406 -0
- praisonai/persistence/state/__init__.py +21 -0
- praisonai/persistence/state/async_mongodb.py +200 -0
- praisonai/persistence/state/base.py +107 -0
- praisonai/persistence/state/dynamodb.py +226 -0
- praisonai/persistence/state/firestore.py +175 -0
- praisonai/persistence/state/gcs.py +155 -0
- praisonai/persistence/state/memory.py +245 -0
- praisonai/persistence/state/mongodb.py +158 -0
- praisonai/persistence/state/redis.py +190 -0
- praisonai/persistence/state/upstash.py +144 -0
- praisonai/persistence/tests/__init__.py +3 -0
- praisonai/persistence/tests/test_all_backends.py +633 -0
- praisonai/profiler.py +1214 -0
- praisonai/recipe/__init__.py +134 -0
- praisonai/recipe/bridge.py +278 -0
- praisonai/recipe/core.py +893 -0
- praisonai/recipe/exceptions.py +54 -0
- praisonai/recipe/history.py +402 -0
- praisonai/recipe/models.py +266 -0
- praisonai/recipe/operations.py +440 -0
- praisonai/recipe/policy.py +422 -0
- praisonai/recipe/registry.py +849 -0
- praisonai/recipe/runtime.py +214 -0
- praisonai/recipe/security.py +711 -0
- praisonai/recipe/serve.py +859 -0
- praisonai/recipe/server.py +613 -0
- praisonai/scheduler/__init__.py +45 -0
- praisonai/scheduler/agent_scheduler.py +552 -0
- praisonai/scheduler/base.py +124 -0
- praisonai/scheduler/daemon_manager.py +225 -0
- praisonai/scheduler/state_manager.py +155 -0
- praisonai/scheduler/yaml_loader.py +193 -0
- praisonai/scheduler.py +194 -0
- praisonai/setup/__init__.py +1 -0
- praisonai/setup/build.py +21 -0
- praisonai/setup/post_install.py +23 -0
- praisonai/setup/setup_conda_env.py +25 -0
- praisonai/setup.py +16 -0
- praisonai/templates/__init__.py +116 -0
- praisonai/templates/cache.py +364 -0
- praisonai/templates/dependency_checker.py +358 -0
- praisonai/templates/discovery.py +391 -0
- praisonai/templates/loader.py +564 -0
- praisonai/templates/registry.py +511 -0
- praisonai/templates/resolver.py +206 -0
- praisonai/templates/security.py +327 -0
- praisonai/templates/tool_override.py +498 -0
- praisonai/templates/tools_doctor.py +256 -0
- praisonai/test.py +105 -0
- praisonai/train.py +562 -0
- praisonai/train_vision.py +306 -0
- praisonai/ui/agents.py +824 -0
- praisonai/ui/callbacks.py +57 -0
- praisonai/ui/chainlit_compat.py +246 -0
- praisonai/ui/chat.py +532 -0
- praisonai/ui/code.py +717 -0
- praisonai/ui/colab.py +474 -0
- praisonai/ui/colab_chainlit.py +81 -0
- praisonai/ui/components/aicoder.py +284 -0
- praisonai/ui/context.py +283 -0
- praisonai/ui/database_config.py +56 -0
- praisonai/ui/db.py +294 -0
- praisonai/ui/realtime.py +488 -0
- praisonai/ui/realtimeclient/__init__.py +756 -0
- praisonai/ui/realtimeclient/tools.py +242 -0
- praisonai/ui/sql_alchemy.py +710 -0
- praisonai/upload_vision.py +140 -0
- praisonai/version.py +1 -0
- praisonai-3.0.0.dist-info/METADATA +3493 -0
- praisonai-3.0.0.dist-info/RECORD +393 -0
- praisonai-3.0.0.dist-info/WHEEL +5 -0
- praisonai-3.0.0.dist-info/entry_points.txt +4 -0
- praisonai-3.0.0.dist-info/top_level.txt +1 -0
praisonai/jobs/models.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Job Models for PraisonAI Async Jobs API.
|
|
3
|
+
|
|
4
|
+
Defines the data structures for job submission, status, and results.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import uuid
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Optional, Dict, Any, List
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class JobStatus(str, Enum):
|
|
15
|
+
"""Status of a job."""
|
|
16
|
+
QUEUED = "queued"
|
|
17
|
+
RUNNING = "running"
|
|
18
|
+
SUCCEEDED = "succeeded"
|
|
19
|
+
FAILED = "failed"
|
|
20
|
+
CANCELLED = "cancelled"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class JobSubmitRequest(BaseModel):
|
|
24
|
+
"""Request body for submitting a new job."""
|
|
25
|
+
prompt: str = Field(..., description="The prompt or task for the agent")
|
|
26
|
+
agent_file: Optional[str] = Field(None, description="Path to agents.yaml file")
|
|
27
|
+
agent_yaml: Optional[str] = Field(None, description="Inline agent YAML configuration")
|
|
28
|
+
recipe_name: Optional[str] = Field(None, description="Recipe name to execute (mutually exclusive with agent_file)")
|
|
29
|
+
recipe_config: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Recipe configuration overrides")
|
|
30
|
+
framework: Optional[str] = Field("praisonai", description="Framework to use (praisonai, crewai, autogen)")
|
|
31
|
+
config: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Additional configuration")
|
|
32
|
+
webhook_url: Optional[str] = Field(None, description="URL to POST results when complete")
|
|
33
|
+
timeout: Optional[int] = Field(3600, description="Timeout in seconds (default: 1 hour)")
|
|
34
|
+
session_id: Optional[str] = Field(None, description="Session ID for conversation continuity")
|
|
35
|
+
idempotency_key: Optional[str] = Field(None, description="Idempotency key to prevent duplicate submissions")
|
|
36
|
+
idempotency_scope: Optional[str] = Field("none", description="Idempotency scope: none, session, global")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class JobSubmitResponse(BaseModel):
|
|
40
|
+
"""Response after submitting a job."""
|
|
41
|
+
job_id: str = Field(..., description="Unique job identifier")
|
|
42
|
+
status: JobStatus = Field(..., description="Current job status")
|
|
43
|
+
created_at: datetime = Field(..., description="Job creation timestamp")
|
|
44
|
+
poll_url: str = Field(..., description="URL to poll for status")
|
|
45
|
+
stream_url: str = Field(..., description="URL for SSE streaming")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class JobProgress(BaseModel):
|
|
49
|
+
"""Progress information for a running job."""
|
|
50
|
+
percentage: float = Field(0.0, ge=0.0, le=100.0, description="Completion percentage")
|
|
51
|
+
current_step: Optional[str] = Field(None, description="Current step description")
|
|
52
|
+
steps_completed: int = Field(0, description="Number of steps completed")
|
|
53
|
+
steps_total: Optional[int] = Field(None, description="Total number of steps")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class JobStatusResponse(BaseModel):
|
|
57
|
+
"""Response for job status query."""
|
|
58
|
+
job_id: str = Field(..., description="Unique job identifier")
|
|
59
|
+
status: JobStatus = Field(..., description="Current job status")
|
|
60
|
+
progress: JobProgress = Field(default_factory=JobProgress, description="Progress information")
|
|
61
|
+
created_at: datetime = Field(..., description="Job creation timestamp")
|
|
62
|
+
started_at: Optional[datetime] = Field(None, description="When job started running")
|
|
63
|
+
completed_at: Optional[datetime] = Field(None, description="When job completed")
|
|
64
|
+
agent_id: Optional[str] = Field(None, description="Current agent ID")
|
|
65
|
+
run_id: Optional[str] = Field(None, description="Run ID for tracing")
|
|
66
|
+
session_id: Optional[str] = Field(None, description="Session ID")
|
|
67
|
+
error: Optional[str] = Field(None, description="Error message if failed")
|
|
68
|
+
retry_after: Optional[int] = Field(None, description="Suggested seconds before next poll")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class JobResultResponse(BaseModel):
|
|
72
|
+
"""Response for job result query."""
|
|
73
|
+
job_id: str = Field(..., description="Unique job identifier")
|
|
74
|
+
status: JobStatus = Field(..., description="Final job status")
|
|
75
|
+
result: Optional[Any] = Field(None, description="Job result/output")
|
|
76
|
+
result_url: Optional[str] = Field(None, description="URL to fetch large results")
|
|
77
|
+
metrics: Optional[Dict[str, Any]] = Field(None, description="Execution metrics")
|
|
78
|
+
created_at: datetime = Field(..., description="Job creation timestamp")
|
|
79
|
+
started_at: Optional[datetime] = Field(None, description="When job started running")
|
|
80
|
+
completed_at: Optional[datetime] = Field(None, description="When job completed")
|
|
81
|
+
duration_seconds: Optional[float] = Field(None, description="Total execution time")
|
|
82
|
+
error: Optional[str] = Field(None, description="Error message if failed")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class JobListResponse(BaseModel):
|
|
86
|
+
"""Response for listing jobs."""
|
|
87
|
+
jobs: List[JobStatusResponse] = Field(..., description="List of jobs")
|
|
88
|
+
total: int = Field(..., description="Total number of jobs matching filter")
|
|
89
|
+
page: int = Field(1, description="Current page number")
|
|
90
|
+
page_size: int = Field(20, description="Number of jobs per page")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Job(BaseModel):
|
|
94
|
+
"""Internal job representation with full state."""
|
|
95
|
+
id: str = Field(default_factory=lambda: f"run_{uuid.uuid4().hex[:12]}")
|
|
96
|
+
status: JobStatus = Field(default=JobStatus.QUEUED)
|
|
97
|
+
|
|
98
|
+
# Request data
|
|
99
|
+
prompt: str = Field(...)
|
|
100
|
+
agent_file: Optional[str] = Field(None)
|
|
101
|
+
agent_yaml: Optional[str] = Field(None)
|
|
102
|
+
recipe_name: Optional[str] = Field(None)
|
|
103
|
+
recipe_config: Dict[str, Any] = Field(default_factory=dict)
|
|
104
|
+
framework: str = Field("praisonai")
|
|
105
|
+
config: Dict[str, Any] = Field(default_factory=dict)
|
|
106
|
+
webhook_url: Optional[str] = Field(None)
|
|
107
|
+
timeout: int = Field(3600)
|
|
108
|
+
session_id: Optional[str] = Field(None)
|
|
109
|
+
idempotency_key: Optional[str] = Field(None)
|
|
110
|
+
idempotency_scope: str = Field("none")
|
|
111
|
+
|
|
112
|
+
# Progress tracking
|
|
113
|
+
progress_percentage: float = Field(0.0)
|
|
114
|
+
progress_step: Optional[str] = Field(None)
|
|
115
|
+
steps_completed: int = Field(0)
|
|
116
|
+
steps_total: Optional[int] = Field(None)
|
|
117
|
+
|
|
118
|
+
# Attribution
|
|
119
|
+
agent_id: Optional[str] = Field(None)
|
|
120
|
+
run_id: Optional[str] = Field(None)
|
|
121
|
+
|
|
122
|
+
# Timestamps
|
|
123
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
124
|
+
started_at: Optional[datetime] = Field(None)
|
|
125
|
+
completed_at: Optional[datetime] = Field(None)
|
|
126
|
+
|
|
127
|
+
# Result
|
|
128
|
+
result: Optional[Any] = Field(None)
|
|
129
|
+
error: Optional[str] = Field(None)
|
|
130
|
+
metrics: Optional[Dict[str, Any]] = Field(None)
|
|
131
|
+
|
|
132
|
+
# Internal
|
|
133
|
+
_cancel_requested: bool = False
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def duration_seconds(self) -> Optional[float]:
|
|
137
|
+
"""Calculate job duration."""
|
|
138
|
+
if self.started_at is None:
|
|
139
|
+
return None
|
|
140
|
+
end_time = self.completed_at or datetime.utcnow()
|
|
141
|
+
return (end_time - self.started_at).total_seconds()
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def is_terminal(self) -> bool:
|
|
145
|
+
"""Check if job is in a terminal state."""
|
|
146
|
+
return self.status in (JobStatus.SUCCEEDED, JobStatus.FAILED, JobStatus.CANCELLED)
|
|
147
|
+
|
|
148
|
+
def to_status_response(self, base_url: str = "") -> JobStatusResponse:
|
|
149
|
+
"""Convert to status response."""
|
|
150
|
+
retry_after = None
|
|
151
|
+
if self.status == JobStatus.QUEUED:
|
|
152
|
+
retry_after = 2
|
|
153
|
+
elif self.status == JobStatus.RUNNING:
|
|
154
|
+
retry_after = 5
|
|
155
|
+
|
|
156
|
+
return JobStatusResponse(
|
|
157
|
+
job_id=self.id,
|
|
158
|
+
status=self.status,
|
|
159
|
+
progress=JobProgress(
|
|
160
|
+
percentage=self.progress_percentage,
|
|
161
|
+
current_step=self.progress_step,
|
|
162
|
+
steps_completed=self.steps_completed,
|
|
163
|
+
steps_total=self.steps_total
|
|
164
|
+
),
|
|
165
|
+
created_at=self.created_at,
|
|
166
|
+
started_at=self.started_at,
|
|
167
|
+
completed_at=self.completed_at,
|
|
168
|
+
agent_id=self.agent_id,
|
|
169
|
+
run_id=self.run_id,
|
|
170
|
+
session_id=self.session_id,
|
|
171
|
+
error=self.error,
|
|
172
|
+
retry_after=retry_after
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def to_result_response(self) -> JobResultResponse:
|
|
176
|
+
"""Convert to result response."""
|
|
177
|
+
return JobResultResponse(
|
|
178
|
+
job_id=self.id,
|
|
179
|
+
status=self.status,
|
|
180
|
+
result=self.result,
|
|
181
|
+
result_url=None, # For large results, would be set
|
|
182
|
+
metrics=self.metrics,
|
|
183
|
+
created_at=self.created_at,
|
|
184
|
+
started_at=self.started_at,
|
|
185
|
+
completed_at=self.completed_at,
|
|
186
|
+
duration_seconds=self.duration_seconds,
|
|
187
|
+
error=self.error
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def start(self):
|
|
191
|
+
"""Mark job as started."""
|
|
192
|
+
self.status = JobStatus.RUNNING
|
|
193
|
+
self.started_at = datetime.utcnow()
|
|
194
|
+
|
|
195
|
+
def succeed(self, result: Any, metrics: Optional[Dict[str, Any]] = None):
|
|
196
|
+
"""Mark job as succeeded."""
|
|
197
|
+
self.status = JobStatus.SUCCEEDED
|
|
198
|
+
self.result = result
|
|
199
|
+
self.metrics = metrics
|
|
200
|
+
self.completed_at = datetime.utcnow()
|
|
201
|
+
self.progress_percentage = 100.0
|
|
202
|
+
|
|
203
|
+
def fail(self, error: str):
|
|
204
|
+
"""Mark job as failed."""
|
|
205
|
+
self.status = JobStatus.FAILED
|
|
206
|
+
self.error = error
|
|
207
|
+
self.completed_at = datetime.utcnow()
|
|
208
|
+
|
|
209
|
+
def cancel(self):
|
|
210
|
+
"""Mark job as cancelled."""
|
|
211
|
+
self.status = JobStatus.CANCELLED
|
|
212
|
+
self.completed_at = datetime.utcnow()
|
|
213
|
+
self._cancel_requested = True
|
|
214
|
+
|
|
215
|
+
def update_progress(
|
|
216
|
+
self,
|
|
217
|
+
percentage: Optional[float] = None,
|
|
218
|
+
step: Optional[str] = None,
|
|
219
|
+
steps_completed: Optional[int] = None,
|
|
220
|
+
steps_total: Optional[int] = None
|
|
221
|
+
):
|
|
222
|
+
"""Update job progress."""
|
|
223
|
+
if percentage is not None:
|
|
224
|
+
self.progress_percentage = min(max(percentage, 0.0), 100.0)
|
|
225
|
+
if step is not None:
|
|
226
|
+
self.progress_step = step
|
|
227
|
+
if steps_completed is not None:
|
|
228
|
+
self.steps_completed = steps_completed
|
|
229
|
+
if steps_total is not None:
|
|
230
|
+
self.steps_total = steps_total
|
praisonai/jobs/router.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI Router for PraisonAI Async Jobs API.
|
|
3
|
+
|
|
4
|
+
Provides HTTP endpoints for job management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, HTTPException, Header, Request, Query, Response
|
|
13
|
+
from sse_starlette.sse import EventSourceResponse
|
|
14
|
+
|
|
15
|
+
from .models import (
|
|
16
|
+
Job,
|
|
17
|
+
JobStatus,
|
|
18
|
+
JobSubmitRequest,
|
|
19
|
+
JobSubmitResponse,
|
|
20
|
+
JobStatusResponse,
|
|
21
|
+
JobResultResponse,
|
|
22
|
+
JobListResponse
|
|
23
|
+
)
|
|
24
|
+
from .store import JobStore
|
|
25
|
+
from .executor import JobExecutor
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def create_router(store: JobStore, executor: JobExecutor) -> APIRouter:
|
|
31
|
+
"""
|
|
32
|
+
Create the jobs API router.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
store: Job storage backend
|
|
36
|
+
executor: Job executor
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
FastAPI router with all endpoints
|
|
40
|
+
"""
|
|
41
|
+
router = APIRouter(prefix="/api/v1/runs", tags=["jobs"])
|
|
42
|
+
|
|
43
|
+
@router.post("", response_model=JobSubmitResponse, status_code=202)
|
|
44
|
+
async def submit_job(
|
|
45
|
+
request: Request,
|
|
46
|
+
response: Response,
|
|
47
|
+
body: JobSubmitRequest,
|
|
48
|
+
idempotency_key: Optional[str] = Header(None, alias="Idempotency-Key")
|
|
49
|
+
):
|
|
50
|
+
"""
|
|
51
|
+
Submit a new job for execution.
|
|
52
|
+
|
|
53
|
+
Returns 202 Accepted with job ID and status URLs.
|
|
54
|
+
Headers returned:
|
|
55
|
+
- Location: URL to poll for status
|
|
56
|
+
- Retry-After: Suggested seconds before first poll (default: 2)
|
|
57
|
+
|
|
58
|
+
If an Idempotency-Key header is provided and a job with that key
|
|
59
|
+
already exists, returns the existing job instead of creating a new one.
|
|
60
|
+
"""
|
|
61
|
+
# Check idempotency key
|
|
62
|
+
effective_key = idempotency_key or body.idempotency_key
|
|
63
|
+
if effective_key:
|
|
64
|
+
existing = await store.get_by_idempotency_key(effective_key)
|
|
65
|
+
if existing:
|
|
66
|
+
logger.info(f"Returning existing job for idempotency key: {effective_key}")
|
|
67
|
+
base_url = str(request.base_url).rstrip("/")
|
|
68
|
+
poll_url = f"{base_url}/api/v1/runs/{existing.id}"
|
|
69
|
+
response.headers["Location"] = poll_url
|
|
70
|
+
response.headers["Retry-After"] = "2"
|
|
71
|
+
return JobSubmitResponse(
|
|
72
|
+
job_id=existing.id,
|
|
73
|
+
status=existing.status,
|
|
74
|
+
created_at=existing.created_at,
|
|
75
|
+
poll_url=poll_url,
|
|
76
|
+
stream_url=f"{base_url}/api/v1/runs/{existing.id}/stream"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Create new job
|
|
80
|
+
job = Job(
|
|
81
|
+
prompt=body.prompt,
|
|
82
|
+
agent_file=body.agent_file,
|
|
83
|
+
agent_yaml=body.agent_yaml,
|
|
84
|
+
framework=body.framework or "praisonai",
|
|
85
|
+
config=body.config or {},
|
|
86
|
+
webhook_url=body.webhook_url,
|
|
87
|
+
timeout=body.timeout or 3600,
|
|
88
|
+
session_id=body.session_id,
|
|
89
|
+
idempotency_key=effective_key
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Submit for execution
|
|
93
|
+
await executor.submit(job)
|
|
94
|
+
|
|
95
|
+
# Build response with Location and Retry-After headers (RFC best practice)
|
|
96
|
+
base_url = str(request.base_url).rstrip("/")
|
|
97
|
+
poll_url = f"{base_url}/api/v1/runs/{job.id}"
|
|
98
|
+
response.headers["Location"] = poll_url
|
|
99
|
+
response.headers["Retry-After"] = "2"
|
|
100
|
+
|
|
101
|
+
return JobSubmitResponse(
|
|
102
|
+
job_id=job.id,
|
|
103
|
+
status=job.status,
|
|
104
|
+
created_at=job.created_at,
|
|
105
|
+
poll_url=poll_url,
|
|
106
|
+
stream_url=f"{base_url}/api/v1/runs/{job.id}/stream"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
@router.get("", response_model=JobListResponse)
|
|
110
|
+
async def list_jobs(
|
|
111
|
+
status: Optional[str] = Query(None, description="Filter by status"),
|
|
112
|
+
session_id: Optional[str] = Query(None, description="Filter by session ID"),
|
|
113
|
+
page: int = Query(1, ge=1, description="Page number"),
|
|
114
|
+
page_size: int = Query(20, ge=1, le=100, description="Jobs per page")
|
|
115
|
+
):
|
|
116
|
+
"""
|
|
117
|
+
List jobs with optional filters.
|
|
118
|
+
"""
|
|
119
|
+
# Parse status filter
|
|
120
|
+
status_filter = None
|
|
121
|
+
if status:
|
|
122
|
+
try:
|
|
123
|
+
status_filter = JobStatus(status)
|
|
124
|
+
except ValueError:
|
|
125
|
+
raise HTTPException(
|
|
126
|
+
status_code=400,
|
|
127
|
+
detail=f"Invalid status: {status}. Valid values: {[s.value for s in JobStatus]}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Get jobs
|
|
131
|
+
offset = (page - 1) * page_size
|
|
132
|
+
jobs = await store.list_jobs(
|
|
133
|
+
status=status_filter,
|
|
134
|
+
session_id=session_id,
|
|
135
|
+
limit=page_size,
|
|
136
|
+
offset=offset
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
total = await store.count(status=status_filter, session_id=session_id)
|
|
140
|
+
|
|
141
|
+
return JobListResponse(
|
|
142
|
+
jobs=[job.to_status_response() for job in jobs],
|
|
143
|
+
total=total,
|
|
144
|
+
page=page,
|
|
145
|
+
page_size=page_size
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
@router.get("/{job_id}", response_model=JobStatusResponse)
|
|
149
|
+
async def get_job_status(job_id: str):
|
|
150
|
+
"""
|
|
151
|
+
Get the status of a job.
|
|
152
|
+
|
|
153
|
+
Returns current status, progress, and timing information.
|
|
154
|
+
"""
|
|
155
|
+
job = await store.get(job_id)
|
|
156
|
+
if not job:
|
|
157
|
+
raise HTTPException(status_code=404, detail=f"Job not found: {job_id}")
|
|
158
|
+
|
|
159
|
+
return job.to_status_response()
|
|
160
|
+
|
|
161
|
+
@router.get("/{job_id}/result", response_model=JobResultResponse)
|
|
162
|
+
async def get_job_result(job_id: str):
|
|
163
|
+
"""
|
|
164
|
+
Get the result of a completed job.
|
|
165
|
+
|
|
166
|
+
Returns 409 Conflict if job is not yet complete.
|
|
167
|
+
"""
|
|
168
|
+
job = await store.get(job_id)
|
|
169
|
+
if not job:
|
|
170
|
+
raise HTTPException(status_code=404, detail=f"Job not found: {job_id}")
|
|
171
|
+
|
|
172
|
+
if not job.is_terminal:
|
|
173
|
+
raise HTTPException(
|
|
174
|
+
status_code=409,
|
|
175
|
+
detail=f"Job is not complete. Current status: {job.status.value}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return job.to_result_response()
|
|
179
|
+
|
|
180
|
+
@router.post("/{job_id}/cancel", response_model=JobStatusResponse)
|
|
181
|
+
async def cancel_job(job_id: str):
|
|
182
|
+
"""
|
|
183
|
+
Cancel a running job.
|
|
184
|
+
|
|
185
|
+
Returns 409 Conflict if job is already complete.
|
|
186
|
+
"""
|
|
187
|
+
job = await store.get(job_id)
|
|
188
|
+
if not job:
|
|
189
|
+
raise HTTPException(status_code=404, detail=f"Job not found: {job_id}")
|
|
190
|
+
|
|
191
|
+
if job.is_terminal:
|
|
192
|
+
raise HTTPException(
|
|
193
|
+
status_code=409,
|
|
194
|
+
detail=f"Job is already complete. Status: {job.status.value}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
success = await executor.cancel(job_id)
|
|
198
|
+
if not success:
|
|
199
|
+
raise HTTPException(status_code=500, detail="Failed to cancel job")
|
|
200
|
+
|
|
201
|
+
# Refresh job state
|
|
202
|
+
job = await store.get(job_id)
|
|
203
|
+
return job.to_status_response()
|
|
204
|
+
|
|
205
|
+
@router.delete("/{job_id}", status_code=204)
|
|
206
|
+
async def delete_job(job_id: str):
|
|
207
|
+
"""
|
|
208
|
+
Delete a job.
|
|
209
|
+
|
|
210
|
+
Only completed jobs can be deleted.
|
|
211
|
+
"""
|
|
212
|
+
job = await store.get(job_id)
|
|
213
|
+
if not job:
|
|
214
|
+
raise HTTPException(status_code=404, detail=f"Job not found: {job_id}")
|
|
215
|
+
|
|
216
|
+
if not job.is_terminal:
|
|
217
|
+
raise HTTPException(
|
|
218
|
+
status_code=409,
|
|
219
|
+
detail="Cannot delete a running job. Cancel it first."
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
await store.delete(job_id)
|
|
223
|
+
|
|
224
|
+
@router.get("/{job_id}/stream")
|
|
225
|
+
async def stream_job(job_id: str):
|
|
226
|
+
"""
|
|
227
|
+
Stream job progress via Server-Sent Events (SSE).
|
|
228
|
+
|
|
229
|
+
Events:
|
|
230
|
+
- status: Job status updates
|
|
231
|
+
- progress: Progress percentage updates
|
|
232
|
+
- result: Final result (when complete)
|
|
233
|
+
- error: Error message (when failed)
|
|
234
|
+
"""
|
|
235
|
+
job = await store.get(job_id)
|
|
236
|
+
if not job:
|
|
237
|
+
raise HTTPException(status_code=404, detail=f"Job not found: {job_id}")
|
|
238
|
+
|
|
239
|
+
async def event_generator():
|
|
240
|
+
"""Generate SSE events for job progress."""
|
|
241
|
+
last_status = None
|
|
242
|
+
last_progress = -1
|
|
243
|
+
|
|
244
|
+
# Register for progress updates
|
|
245
|
+
progress_queue = asyncio.Queue()
|
|
246
|
+
|
|
247
|
+
async def on_progress(updated_job: Job):
|
|
248
|
+
await progress_queue.put(updated_job)
|
|
249
|
+
|
|
250
|
+
executor.register_progress_callback(job_id, on_progress)
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
while True:
|
|
254
|
+
# Get current job state
|
|
255
|
+
current_job = await store.get(job_id)
|
|
256
|
+
if not current_job:
|
|
257
|
+
yield {
|
|
258
|
+
"event": "error",
|
|
259
|
+
"data": '{"error": "Job not found"}'
|
|
260
|
+
}
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
# Send status update if changed
|
|
264
|
+
if current_job.status != last_status:
|
|
265
|
+
last_status = current_job.status
|
|
266
|
+
yield {
|
|
267
|
+
"event": "status",
|
|
268
|
+
"data": f'{{"status": "{current_job.status.value}", "job_id": "{job_id}"}}'
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# Send progress update if changed
|
|
272
|
+
if current_job.progress_percentage != last_progress:
|
|
273
|
+
last_progress = current_job.progress_percentage
|
|
274
|
+
yield {
|
|
275
|
+
"event": "progress",
|
|
276
|
+
"data": f'{{"percentage": {current_job.progress_percentage}, "step": "{current_job.progress_step or ""}"}}'
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# Check if complete
|
|
280
|
+
if current_job.is_terminal:
|
|
281
|
+
if current_job.status == JobStatus.SUCCEEDED:
|
|
282
|
+
result_str = str(current_job.result).replace('"', '\\"')[:1000]
|
|
283
|
+
yield {
|
|
284
|
+
"event": "result",
|
|
285
|
+
"data": f'{{"result": "{result_str}"}}'
|
|
286
|
+
}
|
|
287
|
+
elif current_job.status == JobStatus.FAILED:
|
|
288
|
+
error_str = (current_job.error or "Unknown error").replace('"', '\\"')
|
|
289
|
+
yield {
|
|
290
|
+
"event": "error",
|
|
291
|
+
"data": f'{{"error": "{error_str}"}}'
|
|
292
|
+
}
|
|
293
|
+
elif current_job.status == JobStatus.CANCELLED:
|
|
294
|
+
yield {
|
|
295
|
+
"event": "cancelled",
|
|
296
|
+
"data": '{"message": "Job was cancelled"}'
|
|
297
|
+
}
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
# Wait for next update or timeout
|
|
301
|
+
try:
|
|
302
|
+
await asyncio.wait_for(progress_queue.get(), timeout=5.0)
|
|
303
|
+
except asyncio.TimeoutError:
|
|
304
|
+
# Send heartbeat
|
|
305
|
+
yield {
|
|
306
|
+
"event": "heartbeat",
|
|
307
|
+
"data": f'{{"timestamp": "{datetime.utcnow().isoformat()}"}}'
|
|
308
|
+
}
|
|
309
|
+
finally:
|
|
310
|
+
executor.unregister_progress_callback(job_id)
|
|
311
|
+
|
|
312
|
+
return EventSourceResponse(event_generator())
|
|
313
|
+
|
|
314
|
+
return router
|