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
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Performance Optimizations for PraisonAI CLI.
|
|
3
|
+
|
|
4
|
+
Implements Tier 0/1/2 optimizations:
|
|
5
|
+
- Tier 0: Lazy imports, provider caching, CLI startup optimization
|
|
6
|
+
- Tier 1: Connection pooling, prewarm hooks (opt-in)
|
|
7
|
+
- Tier 2: Lite mode, async init, perf snapshot
|
|
8
|
+
|
|
9
|
+
All optimizations are safe-by-default and opt-in where behavior changes.
|
|
10
|
+
Multi-agent safe: no global mutable state that affects concurrent runs.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import json
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
20
|
+
from functools import lru_cache
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# =============================================================================
|
|
24
|
+
# TIER 0: Safe Fast Wins (No behavior change)
|
|
25
|
+
# =============================================================================
|
|
26
|
+
|
|
27
|
+
class ProviderCache:
|
|
28
|
+
"""
|
|
29
|
+
Thread-safe cache for provider/model resolution.
|
|
30
|
+
|
|
31
|
+
Caches resolved provider configurations to avoid repeated lookups.
|
|
32
|
+
Per-process cache with clear invalidation rules.
|
|
33
|
+
Multi-agent safe: uses thread-local storage for mutable state.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
_instance = None
|
|
37
|
+
_lock = threading.Lock()
|
|
38
|
+
|
|
39
|
+
def __new__(cls):
|
|
40
|
+
if cls._instance is None:
|
|
41
|
+
with cls._lock:
|
|
42
|
+
if cls._instance is None:
|
|
43
|
+
cls._instance = super().__new__(cls)
|
|
44
|
+
cls._instance._cache = {}
|
|
45
|
+
cls._instance._cache_lock = threading.Lock()
|
|
46
|
+
cls._instance._hits = 0
|
|
47
|
+
cls._instance._misses = 0
|
|
48
|
+
return cls._instance
|
|
49
|
+
|
|
50
|
+
def get(self, key: str) -> Optional[Any]:
|
|
51
|
+
"""Get cached value (thread-safe)."""
|
|
52
|
+
with self._cache_lock:
|
|
53
|
+
if key in self._cache:
|
|
54
|
+
self._hits += 1
|
|
55
|
+
return self._cache[key]
|
|
56
|
+
self._misses += 1
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
def set(self, key: str, value: Any, ttl_seconds: int = 300) -> None:
|
|
60
|
+
"""Set cached value with TTL (thread-safe)."""
|
|
61
|
+
with self._cache_lock:
|
|
62
|
+
self._cache[key] = {
|
|
63
|
+
'value': value,
|
|
64
|
+
'expires': time.time() + ttl_seconds,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def invalidate(self, key: Optional[str] = None) -> None:
|
|
68
|
+
"""Invalidate cache entry or entire cache."""
|
|
69
|
+
with self._cache_lock:
|
|
70
|
+
if key:
|
|
71
|
+
self._cache.pop(key, None)
|
|
72
|
+
else:
|
|
73
|
+
self._cache.clear()
|
|
74
|
+
|
|
75
|
+
def get_stats(self) -> Dict[str, int]:
|
|
76
|
+
"""Get cache statistics."""
|
|
77
|
+
with self._cache_lock:
|
|
78
|
+
return {
|
|
79
|
+
'hits': self._hits,
|
|
80
|
+
'misses': self._misses,
|
|
81
|
+
'size': len(self._cache),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def cleanup_expired(self) -> int:
|
|
85
|
+
"""Remove expired entries. Returns count of removed entries."""
|
|
86
|
+
now = time.time()
|
|
87
|
+
removed = 0
|
|
88
|
+
with self._cache_lock:
|
|
89
|
+
expired_keys = [
|
|
90
|
+
k for k, v in self._cache.items()
|
|
91
|
+
if isinstance(v, dict) and v.get('expires', float('inf')) < now
|
|
92
|
+
]
|
|
93
|
+
for k in expired_keys:
|
|
94
|
+
del self._cache[k]
|
|
95
|
+
removed += 1
|
|
96
|
+
return removed
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Global provider cache instance (singleton, thread-safe)
|
|
100
|
+
_provider_cache = None
|
|
101
|
+
|
|
102
|
+
def get_provider_cache() -> ProviderCache:
|
|
103
|
+
"""Get the global provider cache instance."""
|
|
104
|
+
global _provider_cache
|
|
105
|
+
if _provider_cache is None:
|
|
106
|
+
_provider_cache = ProviderCache()
|
|
107
|
+
return _provider_cache
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@lru_cache(maxsize=32)
|
|
111
|
+
def resolve_model_provider(model: str) -> Tuple[str, str]:
|
|
112
|
+
"""
|
|
113
|
+
Resolve model string to (provider, model_name) tuple.
|
|
114
|
+
|
|
115
|
+
Cached using LRU cache for repeated lookups.
|
|
116
|
+
"""
|
|
117
|
+
if '/' in model:
|
|
118
|
+
parts = model.split('/', 1)
|
|
119
|
+
return parts[0], parts[1]
|
|
120
|
+
|
|
121
|
+
# Default provider mappings
|
|
122
|
+
if model.startswith('gpt-') or model.startswith('o1-') or model.startswith('o3-'):
|
|
123
|
+
return 'openai', model
|
|
124
|
+
elif model.startswith('claude-'):
|
|
125
|
+
return 'anthropic', model
|
|
126
|
+
elif model.startswith('gemini-'):
|
|
127
|
+
return 'google', model
|
|
128
|
+
elif model.startswith('llama') or model.startswith('mistral'):
|
|
129
|
+
return 'ollama', model
|
|
130
|
+
|
|
131
|
+
return 'openai', model # Default to OpenAI
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class LazyImporter:
|
|
135
|
+
"""
|
|
136
|
+
Lazy importer for heavy modules.
|
|
137
|
+
|
|
138
|
+
Defers import until first use, reducing CLI startup time.
|
|
139
|
+
Thread-safe and caches imported modules.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
_imports: Dict[str, Any] = {}
|
|
143
|
+
_lock = threading.Lock()
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def get(cls, module_path: str, attr: Optional[str] = None) -> Any:
|
|
147
|
+
"""
|
|
148
|
+
Lazily import a module or attribute.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
module_path: Full module path (e.g., 'openai.OpenAI')
|
|
152
|
+
attr: Optional attribute to get from module
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Imported module or attribute
|
|
156
|
+
"""
|
|
157
|
+
cache_key = f"{module_path}.{attr}" if attr else module_path
|
|
158
|
+
|
|
159
|
+
with cls._lock:
|
|
160
|
+
if cache_key in cls._imports:
|
|
161
|
+
return cls._imports[cache_key]
|
|
162
|
+
|
|
163
|
+
# Import outside lock to avoid deadlocks
|
|
164
|
+
import importlib
|
|
165
|
+
|
|
166
|
+
if attr:
|
|
167
|
+
module = importlib.import_module(module_path)
|
|
168
|
+
result = getattr(module, attr)
|
|
169
|
+
else:
|
|
170
|
+
result = importlib.import_module(module_path)
|
|
171
|
+
|
|
172
|
+
with cls._lock:
|
|
173
|
+
cls._imports[cache_key] = result
|
|
174
|
+
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def clear(cls) -> None:
|
|
179
|
+
"""Clear import cache."""
|
|
180
|
+
with cls._lock:
|
|
181
|
+
cls._imports.clear()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# =============================================================================
|
|
185
|
+
# TIER 1: Medium Effort (Guarded/opt-in)
|
|
186
|
+
# =============================================================================
|
|
187
|
+
|
|
188
|
+
class ClientPool:
|
|
189
|
+
"""
|
|
190
|
+
Connection pool for API clients.
|
|
191
|
+
|
|
192
|
+
Reuses client instances for repeated calls to the same provider.
|
|
193
|
+
Multi-agent safe: clients are isolated by key.
|
|
194
|
+
Does not leak API keys between different configurations.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(self, max_size: int = 10):
|
|
198
|
+
self._pool: Dict[str, Any] = {}
|
|
199
|
+
self._lock = threading.Lock()
|
|
200
|
+
self._max_size = max_size
|
|
201
|
+
self._access_times: Dict[str, float] = {}
|
|
202
|
+
|
|
203
|
+
def get_or_create(
|
|
204
|
+
self,
|
|
205
|
+
key: str,
|
|
206
|
+
factory: Callable[[], Any],
|
|
207
|
+
) -> Any:
|
|
208
|
+
"""
|
|
209
|
+
Get existing client or create new one.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
key: Unique key for this client configuration
|
|
213
|
+
factory: Function to create new client if needed
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Client instance
|
|
217
|
+
"""
|
|
218
|
+
with self._lock:
|
|
219
|
+
if key in self._pool:
|
|
220
|
+
self._access_times[key] = time.time()
|
|
221
|
+
return self._pool[key]
|
|
222
|
+
|
|
223
|
+
# Evict oldest if at capacity
|
|
224
|
+
if len(self._pool) >= self._max_size:
|
|
225
|
+
oldest_key = min(self._access_times, key=self._access_times.get)
|
|
226
|
+
del self._pool[oldest_key]
|
|
227
|
+
del self._access_times[oldest_key]
|
|
228
|
+
|
|
229
|
+
# Create outside lock
|
|
230
|
+
client = factory()
|
|
231
|
+
|
|
232
|
+
with self._lock:
|
|
233
|
+
self._pool[key] = client
|
|
234
|
+
self._access_times[key] = time.time()
|
|
235
|
+
|
|
236
|
+
return client
|
|
237
|
+
|
|
238
|
+
def remove(self, key: str) -> None:
|
|
239
|
+
"""Remove client from pool."""
|
|
240
|
+
with self._lock:
|
|
241
|
+
self._pool.pop(key, None)
|
|
242
|
+
self._access_times.pop(key, None)
|
|
243
|
+
|
|
244
|
+
def clear(self) -> None:
|
|
245
|
+
"""Clear all clients from pool."""
|
|
246
|
+
with self._lock:
|
|
247
|
+
self._pool.clear()
|
|
248
|
+
self._access_times.clear()
|
|
249
|
+
|
|
250
|
+
def size(self) -> int:
|
|
251
|
+
"""Get current pool size."""
|
|
252
|
+
with self._lock:
|
|
253
|
+
return len(self._pool)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# Global client pool (opt-in usage)
|
|
257
|
+
_client_pool: Optional[ClientPool] = None
|
|
258
|
+
|
|
259
|
+
def get_client_pool() -> ClientPool:
|
|
260
|
+
"""Get the global client pool instance."""
|
|
261
|
+
global _client_pool
|
|
262
|
+
if _client_pool is None:
|
|
263
|
+
_client_pool = ClientPool()
|
|
264
|
+
return _client_pool
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class PrewarmManager:
|
|
268
|
+
"""
|
|
269
|
+
Manager for pre-warming provider connections.
|
|
270
|
+
|
|
271
|
+
OPT-IN ONLY: Must be explicitly enabled.
|
|
272
|
+
Pre-initializes provider clients in background to reduce first-call latency.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
_enabled = False
|
|
276
|
+
_prewarmed: Dict[str, bool] = {}
|
|
277
|
+
_lock = threading.Lock()
|
|
278
|
+
_background_thread: Optional[threading.Thread] = None
|
|
279
|
+
|
|
280
|
+
@classmethod
|
|
281
|
+
def enable(cls) -> None:
|
|
282
|
+
"""Enable pre-warming (opt-in)."""
|
|
283
|
+
cls._enabled = True
|
|
284
|
+
|
|
285
|
+
@classmethod
|
|
286
|
+
def disable(cls) -> None:
|
|
287
|
+
"""Disable pre-warming."""
|
|
288
|
+
cls._enabled = False
|
|
289
|
+
|
|
290
|
+
@classmethod
|
|
291
|
+
def is_enabled(cls) -> bool:
|
|
292
|
+
"""Check if pre-warming is enabled."""
|
|
293
|
+
return cls._enabled
|
|
294
|
+
|
|
295
|
+
@classmethod
|
|
296
|
+
def prewarm_provider(cls, provider: str, api_key: Optional[str] = None) -> None:
|
|
297
|
+
"""
|
|
298
|
+
Pre-warm a provider connection in background.
|
|
299
|
+
|
|
300
|
+
Only runs if pre-warming is enabled.
|
|
301
|
+
"""
|
|
302
|
+
if not cls._enabled:
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
with cls._lock:
|
|
306
|
+
if provider in cls._prewarmed:
|
|
307
|
+
return
|
|
308
|
+
cls._prewarmed[provider] = False
|
|
309
|
+
|
|
310
|
+
def _prewarm():
|
|
311
|
+
try:
|
|
312
|
+
if provider == 'openai':
|
|
313
|
+
# Just import and create client - don't make API call
|
|
314
|
+
from openai import OpenAI
|
|
315
|
+
key = api_key or os.environ.get('OPENAI_API_KEY')
|
|
316
|
+
if key:
|
|
317
|
+
_ = OpenAI(api_key=key)
|
|
318
|
+
elif provider == 'anthropic':
|
|
319
|
+
from anthropic import Anthropic
|
|
320
|
+
key = api_key or os.environ.get('ANTHROPIC_API_KEY')
|
|
321
|
+
if key:
|
|
322
|
+
_ = Anthropic(api_key=key)
|
|
323
|
+
|
|
324
|
+
with cls._lock:
|
|
325
|
+
cls._prewarmed[provider] = True
|
|
326
|
+
except Exception:
|
|
327
|
+
pass # Silently fail - pre-warming is optional
|
|
328
|
+
|
|
329
|
+
# Run in background thread
|
|
330
|
+
thread = threading.Thread(target=_prewarm, daemon=True)
|
|
331
|
+
thread.start()
|
|
332
|
+
|
|
333
|
+
@classmethod
|
|
334
|
+
def is_prewarmed(cls, provider: str) -> bool:
|
|
335
|
+
"""Check if provider is pre-warmed."""
|
|
336
|
+
with cls._lock:
|
|
337
|
+
return cls._prewarmed.get(provider, False)
|
|
338
|
+
|
|
339
|
+
@classmethod
|
|
340
|
+
def reset(cls) -> None:
|
|
341
|
+
"""Reset pre-warm state."""
|
|
342
|
+
with cls._lock:
|
|
343
|
+
cls._prewarmed.clear()
|
|
344
|
+
cls._enabled = False
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# =============================================================================
|
|
348
|
+
# TIER 2: Architectural (Safe and non-breaking)
|
|
349
|
+
# =============================================================================
|
|
350
|
+
|
|
351
|
+
@dataclass
|
|
352
|
+
class LiteModeConfig:
|
|
353
|
+
"""
|
|
354
|
+
Configuration for lite runtime mode.
|
|
355
|
+
|
|
356
|
+
OPT-IN ONLY: Defaults to OFF.
|
|
357
|
+
Avoids expensive type/model loading when not required.
|
|
358
|
+
"""
|
|
359
|
+
enabled: bool = False
|
|
360
|
+
skip_type_validation: bool = False
|
|
361
|
+
skip_model_validation: bool = False
|
|
362
|
+
minimal_imports: bool = False
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
def from_env(cls) -> 'LiteModeConfig':
|
|
366
|
+
"""Create config from environment variables."""
|
|
367
|
+
return cls(
|
|
368
|
+
enabled=os.environ.get('PRAISONAI_LITE_MODE', '').lower() in ('1', 'true', 'yes'),
|
|
369
|
+
skip_type_validation=os.environ.get('PRAISONAI_SKIP_TYPE_VALIDATION', '').lower() in ('1', 'true'),
|
|
370
|
+
skip_model_validation=os.environ.get('PRAISONAI_SKIP_MODEL_VALIDATION', '').lower() in ('1', 'true'),
|
|
371
|
+
minimal_imports=os.environ.get('PRAISONAI_MINIMAL_IMPORTS', '').lower() in ('1', 'true'),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# Global lite mode config
|
|
376
|
+
_lite_mode_config: Optional[LiteModeConfig] = None
|
|
377
|
+
|
|
378
|
+
def get_lite_mode_config() -> LiteModeConfig:
|
|
379
|
+
"""Get lite mode configuration."""
|
|
380
|
+
global _lite_mode_config
|
|
381
|
+
if _lite_mode_config is None:
|
|
382
|
+
_lite_mode_config = LiteModeConfig.from_env()
|
|
383
|
+
return _lite_mode_config
|
|
384
|
+
|
|
385
|
+
def is_lite_mode() -> bool:
|
|
386
|
+
"""Check if lite mode is enabled."""
|
|
387
|
+
return get_lite_mode_config().enabled
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@dataclass
|
|
391
|
+
class PerfSnapshot:
|
|
392
|
+
"""
|
|
393
|
+
Performance snapshot for baseline comparison.
|
|
394
|
+
|
|
395
|
+
Records timing data that can be compared against later runs.
|
|
396
|
+
"""
|
|
397
|
+
timestamp: str
|
|
398
|
+
name: str
|
|
399
|
+
startup_cold_ms: float
|
|
400
|
+
startup_warm_ms: float
|
|
401
|
+
import_time_ms: float
|
|
402
|
+
query_time_ms: float
|
|
403
|
+
first_token_ms: float
|
|
404
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
405
|
+
|
|
406
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
407
|
+
return {
|
|
408
|
+
'timestamp': self.timestamp,
|
|
409
|
+
'name': self.name,
|
|
410
|
+
'startup_cold_ms': self.startup_cold_ms,
|
|
411
|
+
'startup_warm_ms': self.startup_warm_ms,
|
|
412
|
+
'import_time_ms': self.import_time_ms,
|
|
413
|
+
'query_time_ms': self.query_time_ms,
|
|
414
|
+
'first_token_ms': self.first_token_ms,
|
|
415
|
+
'metadata': self.metadata,
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
@classmethod
|
|
419
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'PerfSnapshot':
|
|
420
|
+
return cls(
|
|
421
|
+
timestamp=data['timestamp'],
|
|
422
|
+
name=data['name'],
|
|
423
|
+
startup_cold_ms=data['startup_cold_ms'],
|
|
424
|
+
startup_warm_ms=data['startup_warm_ms'],
|
|
425
|
+
import_time_ms=data['import_time_ms'],
|
|
426
|
+
query_time_ms=data['query_time_ms'],
|
|
427
|
+
first_token_ms=data.get('first_token_ms', 0.0),
|
|
428
|
+
metadata=data.get('metadata', {}),
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
@dataclass
|
|
433
|
+
class PerfComparison:
|
|
434
|
+
"""Comparison between two performance snapshots."""
|
|
435
|
+
baseline: PerfSnapshot
|
|
436
|
+
current: PerfSnapshot
|
|
437
|
+
|
|
438
|
+
@property
|
|
439
|
+
def startup_cold_diff_ms(self) -> float:
|
|
440
|
+
return self.current.startup_cold_ms - self.baseline.startup_cold_ms
|
|
441
|
+
|
|
442
|
+
@property
|
|
443
|
+
def startup_cold_diff_pct(self) -> float:
|
|
444
|
+
if self.baseline.startup_cold_ms == 0:
|
|
445
|
+
return 0.0
|
|
446
|
+
return (self.startup_cold_diff_ms / self.baseline.startup_cold_ms) * 100
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def import_time_diff_ms(self) -> float:
|
|
450
|
+
return self.current.import_time_ms - self.baseline.import_time_ms
|
|
451
|
+
|
|
452
|
+
@property
|
|
453
|
+
def import_time_diff_pct(self) -> float:
|
|
454
|
+
if self.baseline.import_time_ms == 0:
|
|
455
|
+
return 0.0
|
|
456
|
+
return (self.import_time_diff_ms / self.baseline.import_time_ms) * 100
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def query_time_diff_ms(self) -> float:
|
|
460
|
+
return self.current.query_time_ms - self.baseline.query_time_ms
|
|
461
|
+
|
|
462
|
+
@property
|
|
463
|
+
def query_time_diff_pct(self) -> float:
|
|
464
|
+
if self.baseline.query_time_ms == 0:
|
|
465
|
+
return 0.0
|
|
466
|
+
return (self.query_time_diff_ms / self.baseline.query_time_ms) * 100
|
|
467
|
+
|
|
468
|
+
def is_regression(self, threshold_pct: float = 10.0) -> bool:
|
|
469
|
+
"""Check if there's a performance regression above threshold."""
|
|
470
|
+
return (
|
|
471
|
+
self.startup_cold_diff_pct > threshold_pct or
|
|
472
|
+
self.import_time_diff_pct > threshold_pct or
|
|
473
|
+
self.query_time_diff_pct > threshold_pct
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
477
|
+
return {
|
|
478
|
+
'baseline': self.baseline.to_dict(),
|
|
479
|
+
'current': self.current.to_dict(),
|
|
480
|
+
'diffs': {
|
|
481
|
+
'startup_cold_ms': self.startup_cold_diff_ms,
|
|
482
|
+
'startup_cold_pct': self.startup_cold_diff_pct,
|
|
483
|
+
'import_time_ms': self.import_time_diff_ms,
|
|
484
|
+
'import_time_pct': self.import_time_diff_pct,
|
|
485
|
+
'query_time_ms': self.query_time_diff_ms,
|
|
486
|
+
'query_time_pct': self.query_time_diff_pct,
|
|
487
|
+
},
|
|
488
|
+
'is_regression': self.is_regression(),
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class PerfSnapshotManager:
|
|
493
|
+
"""
|
|
494
|
+
Manager for performance snapshots.
|
|
495
|
+
|
|
496
|
+
Stores snapshots locally for baseline comparison.
|
|
497
|
+
OPT-IN: User must explicitly invoke snapshot commands.
|
|
498
|
+
"""
|
|
499
|
+
|
|
500
|
+
DEFAULT_DIR = Path.home() / '.praisonai' / 'perf_snapshots'
|
|
501
|
+
|
|
502
|
+
def __init__(self, snapshot_dir: Optional[Path] = None):
|
|
503
|
+
self.snapshot_dir = snapshot_dir or self.DEFAULT_DIR
|
|
504
|
+
self.snapshot_dir.mkdir(parents=True, exist_ok=True)
|
|
505
|
+
|
|
506
|
+
def save(self, snapshot: PerfSnapshot) -> Path:
|
|
507
|
+
"""Save a snapshot to disk."""
|
|
508
|
+
filename = f"{snapshot.name}_{snapshot.timestamp.replace(':', '-').replace('.', '-')}.json"
|
|
509
|
+
path = self.snapshot_dir / filename
|
|
510
|
+
with open(path, 'w') as f:
|
|
511
|
+
json.dump(snapshot.to_dict(), f, indent=2)
|
|
512
|
+
return path
|
|
513
|
+
|
|
514
|
+
def load(self, name: str) -> Optional[PerfSnapshot]:
|
|
515
|
+
"""Load the most recent snapshot with given name."""
|
|
516
|
+
pattern = f"{name}_*.json"
|
|
517
|
+
files = sorted(self.snapshot_dir.glob(pattern), reverse=True)
|
|
518
|
+
if not files:
|
|
519
|
+
return None
|
|
520
|
+
|
|
521
|
+
with open(files[0]) as f:
|
|
522
|
+
data = json.load(f)
|
|
523
|
+
return PerfSnapshot.from_dict(data)
|
|
524
|
+
|
|
525
|
+
def load_baseline(self, name: str = 'baseline') -> Optional[PerfSnapshot]:
|
|
526
|
+
"""Load the baseline snapshot."""
|
|
527
|
+
return self.load(name)
|
|
528
|
+
|
|
529
|
+
def save_baseline(self, snapshot: PerfSnapshot) -> Path:
|
|
530
|
+
"""Save as baseline (overwrites previous baseline)."""
|
|
531
|
+
snapshot.name = 'baseline'
|
|
532
|
+
# Remove old baselines
|
|
533
|
+
for f in self.snapshot_dir.glob('baseline_*.json'):
|
|
534
|
+
f.unlink()
|
|
535
|
+
return self.save(snapshot)
|
|
536
|
+
|
|
537
|
+
def compare(self, current: PerfSnapshot, baseline_name: str = 'baseline') -> Optional[PerfComparison]:
|
|
538
|
+
"""Compare current snapshot against baseline."""
|
|
539
|
+
baseline = self.load(baseline_name)
|
|
540
|
+
if not baseline:
|
|
541
|
+
return None
|
|
542
|
+
return PerfComparison(baseline=baseline, current=current)
|
|
543
|
+
|
|
544
|
+
def list_snapshots(self) -> List[str]:
|
|
545
|
+
"""List all saved snapshot names."""
|
|
546
|
+
files = self.snapshot_dir.glob('*.json')
|
|
547
|
+
names = set()
|
|
548
|
+
for f in files:
|
|
549
|
+
# Extract name from filename (before first underscore)
|
|
550
|
+
name = f.stem.rsplit('_', 1)[0] if '_' in f.stem else f.stem
|
|
551
|
+
names.add(name)
|
|
552
|
+
return sorted(names)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
# =============================================================================
|
|
556
|
+
# Utility Functions
|
|
557
|
+
# =============================================================================
|
|
558
|
+
|
|
559
|
+
def create_snapshot_from_suite(suite_result: Any, name: str = 'current') -> PerfSnapshot:
|
|
560
|
+
"""
|
|
561
|
+
Create a PerfSnapshot from a SuiteResult.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
suite_result: SuiteResult from profile suite
|
|
565
|
+
name: Name for the snapshot
|
|
566
|
+
|
|
567
|
+
Returns:
|
|
568
|
+
PerfSnapshot instance
|
|
569
|
+
"""
|
|
570
|
+
# Get average query time from scenarios
|
|
571
|
+
query_times = []
|
|
572
|
+
first_token_times = []
|
|
573
|
+
for scenario in suite_result.scenarios:
|
|
574
|
+
query_times.extend(scenario.total_times)
|
|
575
|
+
first_token_times.extend(scenario.first_token_times)
|
|
576
|
+
|
|
577
|
+
avg_query_time = sum(query_times) / len(query_times) if query_times else 0.0
|
|
578
|
+
avg_first_token = sum(first_token_times) / len(first_token_times) if first_token_times else 0.0
|
|
579
|
+
|
|
580
|
+
# Get import time from analysis
|
|
581
|
+
import_time = 0.0
|
|
582
|
+
if suite_result.import_analysis:
|
|
583
|
+
import_time = suite_result.import_analysis[0].get('cumulative_ms', 0.0)
|
|
584
|
+
|
|
585
|
+
return PerfSnapshot(
|
|
586
|
+
timestamp=suite_result.timestamp,
|
|
587
|
+
name=name,
|
|
588
|
+
startup_cold_ms=suite_result.startup_cold_ms,
|
|
589
|
+
startup_warm_ms=suite_result.startup_warm_ms,
|
|
590
|
+
import_time_ms=import_time,
|
|
591
|
+
query_time_ms=avg_query_time,
|
|
592
|
+
first_token_ms=avg_first_token,
|
|
593
|
+
metadata=suite_result.metadata,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def format_comparison_report(comparison: PerfComparison) -> str:
|
|
598
|
+
"""Format a comparison report as text."""
|
|
599
|
+
lines = []
|
|
600
|
+
lines.append("=" * 70)
|
|
601
|
+
lines.append("Performance Comparison Report")
|
|
602
|
+
lines.append("=" * 70)
|
|
603
|
+
lines.append("")
|
|
604
|
+
|
|
605
|
+
lines.append(f"Baseline: {comparison.baseline.name} ({comparison.baseline.timestamp})")
|
|
606
|
+
lines.append(f"Current: {comparison.current.name} ({comparison.current.timestamp})")
|
|
607
|
+
lines.append("")
|
|
608
|
+
|
|
609
|
+
lines.append("-" * 70)
|
|
610
|
+
lines.append(f"{'Metric':<25} {'Baseline':>12} {'Current':>12} {'Diff':>12} {'%':>8}")
|
|
611
|
+
lines.append("-" * 70)
|
|
612
|
+
|
|
613
|
+
def format_row(name: str, baseline: float, current: float, diff: float, pct: float) -> str:
|
|
614
|
+
sign = '+' if diff > 0 else ''
|
|
615
|
+
pct_sign = '+' if pct > 0 else ''
|
|
616
|
+
return f"{name:<25} {baseline:>12.2f} {current:>12.2f} {sign}{diff:>11.2f} {pct_sign}{pct:>7.1f}%"
|
|
617
|
+
|
|
618
|
+
lines.append(format_row(
|
|
619
|
+
"Startup Cold (ms)",
|
|
620
|
+
comparison.baseline.startup_cold_ms,
|
|
621
|
+
comparison.current.startup_cold_ms,
|
|
622
|
+
comparison.startup_cold_diff_ms,
|
|
623
|
+
comparison.startup_cold_diff_pct,
|
|
624
|
+
))
|
|
625
|
+
|
|
626
|
+
lines.append(format_row(
|
|
627
|
+
"Import Time (ms)",
|
|
628
|
+
comparison.baseline.import_time_ms,
|
|
629
|
+
comparison.current.import_time_ms,
|
|
630
|
+
comparison.import_time_diff_ms,
|
|
631
|
+
comparison.import_time_diff_pct,
|
|
632
|
+
))
|
|
633
|
+
|
|
634
|
+
lines.append(format_row(
|
|
635
|
+
"Query Time (ms)",
|
|
636
|
+
comparison.baseline.query_time_ms,
|
|
637
|
+
comparison.current.query_time_ms,
|
|
638
|
+
comparison.query_time_diff_ms,
|
|
639
|
+
comparison.query_time_diff_pct,
|
|
640
|
+
))
|
|
641
|
+
|
|
642
|
+
lines.append("-" * 70)
|
|
643
|
+
lines.append("")
|
|
644
|
+
|
|
645
|
+
if comparison.is_regression():
|
|
646
|
+
lines.append("⚠️ REGRESSION DETECTED (>10% slower)")
|
|
647
|
+
else:
|
|
648
|
+
lines.append("✅ No significant regression")
|
|
649
|
+
|
|
650
|
+
lines.append("=" * 70)
|
|
651
|
+
|
|
652
|
+
return "\n".join(lines)
|