devsper 2.1.6__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.
- devsper/__init__.py +14 -0
- devsper/agents/a2a/__init__.py +27 -0
- devsper/agents/a2a/client.py +126 -0
- devsper/agents/a2a/discovery.py +24 -0
- devsper/agents/a2a/server.py +128 -0
- devsper/agents/a2a/tool_adapter.py +68 -0
- devsper/agents/a2a/types.py +49 -0
- devsper/agents/agent.py +602 -0
- devsper/agents/critic.py +80 -0
- devsper/agents/message_bus.py +124 -0
- devsper/agents/roles.py +181 -0
- devsper/agents/run_agent.py +78 -0
- devsper/analytics/__init__.py +5 -0
- devsper/analytics/tool_analytics.py +78 -0
- devsper/audit/__init__.py +5 -0
- devsper/audit/logger.py +214 -0
- devsper/bus/__init__.py +29 -0
- devsper/bus/backends/__init__.py +5 -0
- devsper/bus/backends/base.py +38 -0
- devsper/bus/backends/memory.py +55 -0
- devsper/bus/backends/redis.py +146 -0
- devsper/bus/message.py +56 -0
- devsper/bus/schema_version.py +3 -0
- devsper/bus/topics.py +19 -0
- devsper/cache/__init__.py +6 -0
- devsper/cache/embedding_index.py +98 -0
- devsper/cache/hashing.py +24 -0
- devsper/cache/store.py +153 -0
- devsper/cache/task_cache.py +191 -0
- devsper/cli/__init__.py +6 -0
- devsper/cli/commands/reg.py +733 -0
- devsper/cli/github_oauth.py +157 -0
- devsper/cli/init.py +637 -0
- devsper/cli/main.py +2956 -0
- devsper/cli/run_progress.py +103 -0
- devsper/cli/ui/__init__.py +65 -0
- devsper/cli/ui/components.py +94 -0
- devsper/cli/ui/errors.py +104 -0
- devsper/cli/ui/logging.py +120 -0
- devsper/cli/ui/onboarding.py +102 -0
- devsper/cli/ui/progress.py +43 -0
- devsper/cli/ui/run_view.py +308 -0
- devsper/cli/ui/theme.py +40 -0
- devsper/cluster/__init__.py +29 -0
- devsper/cluster/election.py +84 -0
- devsper/cluster/local.py +97 -0
- devsper/cluster/node_info.py +77 -0
- devsper/cluster/registry.py +71 -0
- devsper/cluster/router.py +117 -0
- devsper/cluster/state_backend.py +105 -0
- devsper/compliance/__init__.py +5 -0
- devsper/compliance/pii.py +147 -0
- devsper/config/__init__.py +52 -0
- devsper/config/config_loader.py +121 -0
- devsper/config/defaults.py +77 -0
- devsper/config/resolver.py +342 -0
- devsper/config/schema.py +237 -0
- devsper/credentials/__init__.py +19 -0
- devsper/credentials/cli.py +197 -0
- devsper/credentials/migration.py +124 -0
- devsper/credentials/store.py +142 -0
- devsper/dashboard/__init__.py +9 -0
- devsper/dashboard/dashboard.py +87 -0
- devsper/dev/__init__.py +25 -0
- devsper/dev/builder.py +195 -0
- devsper/dev/debugger.py +95 -0
- devsper/dev/repo_index.py +138 -0
- devsper/dev/sandbox.py +203 -0
- devsper/dev/scaffold.py +122 -0
- devsper/embeddings/__init__.py +5 -0
- devsper/embeddings/service.py +36 -0
- devsper/explainability/__init__.py +14 -0
- devsper/explainability/decision_tree.py +104 -0
- devsper/explainability/rationale.py +38 -0
- devsper/explainability/simulation.py +56 -0
- devsper/hitl/__init__.py +13 -0
- devsper/hitl/approval.py +160 -0
- devsper/hitl/escalation.py +95 -0
- devsper/intelligence/__init__.py +9 -0
- devsper/intelligence/adaptation.py +88 -0
- devsper/intelligence/analysis/__init__.py +19 -0
- devsper/intelligence/analysis/analyzer.py +71 -0
- devsper/intelligence/analysis/cost_estimator.py +66 -0
- devsper/intelligence/analysis/formatter.py +103 -0
- devsper/intelligence/analysis/run_report.py +402 -0
- devsper/intelligence/learning_engine.py +92 -0
- devsper/intelligence/strategies/__init__.py +23 -0
- devsper/intelligence/strategies/base.py +14 -0
- devsper/intelligence/strategies/code_analysis_strategy.py +33 -0
- devsper/intelligence/strategies/data_science_strategy.py +33 -0
- devsper/intelligence/strategies/document_pipeline_strategy.py +33 -0
- devsper/intelligence/strategies/experiment_strategy.py +33 -0
- devsper/intelligence/strategies/research_strategy.py +34 -0
- devsper/intelligence/strategy_selector.py +84 -0
- devsper/intelligence/synthesis.py +132 -0
- devsper/intelligence/task_optimizer.py +92 -0
- devsper/knowledge/__init__.py +5 -0
- devsper/knowledge/extractor.py +204 -0
- devsper/knowledge/knowledge_graph.py +184 -0
- devsper/knowledge/query.py +285 -0
- devsper/memory/__init__.py +35 -0
- devsper/memory/consolidation.py +138 -0
- devsper/memory/embeddings.py +60 -0
- devsper/memory/memory_index.py +97 -0
- devsper/memory/memory_router.py +62 -0
- devsper/memory/memory_store.py +221 -0
- devsper/memory/memory_types.py +54 -0
- devsper/memory/namespaces.py +45 -0
- devsper/memory/scoring.py +77 -0
- devsper/memory/summarizer.py +52 -0
- devsper/nodes/__init__.py +5 -0
- devsper/nodes/controller.py +449 -0
- devsper/nodes/rpc.py +127 -0
- devsper/nodes/single.py +161 -0
- devsper/nodes/worker.py +506 -0
- devsper/orchestration/__init__.py +19 -0
- devsper/orchestration/meta_planner.py +239 -0
- devsper/orchestration/priority_queue.py +61 -0
- devsper/plugins/__init__.py +19 -0
- devsper/plugins/marketplace/__init__.py +0 -0
- devsper/plugins/plugin_loader.py +70 -0
- devsper/plugins/plugin_registry.py +34 -0
- devsper/plugins/registry.py +83 -0
- devsper/protocols/__init__.py +6 -0
- devsper/providers/__init__.py +17 -0
- devsper/providers/anthropic.py +84 -0
- devsper/providers/base.py +75 -0
- devsper/providers/complexity_router.py +94 -0
- devsper/providers/gemini.py +36 -0
- devsper/providers/github.py +180 -0
- devsper/providers/model_router.py +40 -0
- devsper/providers/openai.py +105 -0
- devsper/providers/router/__init__.py +21 -0
- devsper/providers/router/backends/__init__.py +19 -0
- devsper/providers/router/backends/anthropic_backend.py +111 -0
- devsper/providers/router/backends/custom_backend.py +138 -0
- devsper/providers/router/backends/gemini_backend.py +89 -0
- devsper/providers/router/backends/github_backend.py +165 -0
- devsper/providers/router/backends/ollama_backend.py +104 -0
- devsper/providers/router/backends/openai_backend.py +142 -0
- devsper/providers/router/backends/vllm_backend.py +35 -0
- devsper/providers/router/base.py +60 -0
- devsper/providers/router/factory.py +92 -0
- devsper/providers/router/legacy.py +101 -0
- devsper/providers/router/router.py +135 -0
- devsper/reasoning/__init__.py +12 -0
- devsper/reasoning/graph.py +59 -0
- devsper/reasoning/nodes.py +20 -0
- devsper/reasoning/store.py +67 -0
- devsper/runtime/__init__.py +12 -0
- devsper/runtime/health.py +88 -0
- devsper/runtime/replay.py +53 -0
- devsper/runtime/replay_engine.py +142 -0
- devsper/runtime/run_history.py +204 -0
- devsper/runtime/telemetry.py +116 -0
- devsper/runtime/visualize.py +58 -0
- devsper/sandbox/__init__.py +13 -0
- devsper/sandbox/sandbox.py +161 -0
- devsper/swarm/checkpointer.py +65 -0
- devsper/swarm/executor.py +558 -0
- devsper/swarm/map_reduce.py +44 -0
- devsper/swarm/planner.py +197 -0
- devsper/swarm/prefetcher.py +91 -0
- devsper/swarm/scheduler.py +153 -0
- devsper/swarm/speculation.py +47 -0
- devsper/swarm/swarm.py +562 -0
- devsper/tools/__init__.py +33 -0
- devsper/tools/base.py +29 -0
- devsper/tools/code_intelligence/__init__.py +13 -0
- devsper/tools/code_intelligence/api_surface_extractor.py +73 -0
- devsper/tools/code_intelligence/architecture_analyzer.py +65 -0
- devsper/tools/code_intelligence/codebase_indexer.py +71 -0
- devsper/tools/code_intelligence/dependency_graph_builder.py +67 -0
- devsper/tools/code_intelligence/design_pattern_detector.py +62 -0
- devsper/tools/code_intelligence/large_function_detector.py +68 -0
- devsper/tools/code_intelligence/module_responsibility_mapper.py +56 -0
- devsper/tools/code_intelligence/parallel_codebase_analysis.py +44 -0
- devsper/tools/code_intelligence/refactor_candidate_detector.py +81 -0
- devsper/tools/code_intelligence/repository_semantic_index.py +61 -0
- devsper/tools/code_intelligence/test_coverage_estimator.py +62 -0
- devsper/tools/coding/__init__.py +12 -0
- devsper/tools/coding/analyze_code_complexity.py +48 -0
- devsper/tools/coding/dependency_analyzer.py +42 -0
- devsper/tools/coding/extract_functions.py +38 -0
- devsper/tools/coding/format_python.py +50 -0
- devsper/tools/coding/generate_docstrings.py +40 -0
- devsper/tools/coding/generate_unit_tests.py +42 -0
- devsper/tools/coding/lint_python.py +51 -0
- devsper/tools/coding/refactor_function.py +41 -0
- devsper/tools/coding/repo_structure_map.py +54 -0
- devsper/tools/coding/run_python.py +53 -0
- devsper/tools/data/__init__.py +12 -0
- devsper/tools/data/column_type_detection.py +64 -0
- devsper/tools/data/csv_summary.py +52 -0
- devsper/tools/data/dataframe_filter.py +51 -0
- devsper/tools/data/dataframe_groupby.py +47 -0
- devsper/tools/data/dataframe_stats.py +38 -0
- devsper/tools/data/dataset_sampling.py +55 -0
- devsper/tools/data/dataset_schema.py +45 -0
- devsper/tools/data/json_pretty_print.py +37 -0
- devsper/tools/data/json_query.py +46 -0
- devsper/tools/data/missing_value_report.py +47 -0
- devsper/tools/data_science/__init__.py +13 -0
- devsper/tools/data_science/correlation_heatmap.py +72 -0
- devsper/tools/data_science/dataset_bias_detector.py +49 -0
- devsper/tools/data_science/dataset_distribution_report.py +64 -0
- devsper/tools/data_science/dataset_drift_detector.py +64 -0
- devsper/tools/data_science/dataset_outlier_detector.py +65 -0
- devsper/tools/data_science/dataset_profile.py +76 -0
- devsper/tools/data_science/distributed_dataset_processor.py +54 -0
- devsper/tools/data_science/feature_engineering_suggestions.py +69 -0
- devsper/tools/data_science/feature_importance_estimator.py +82 -0
- devsper/tools/data_science/model_input_validator.py +59 -0
- devsper/tools/data_science/time_series_analyzer.py +57 -0
- devsper/tools/documents/__init__.py +11 -0
- devsper/tools/documents/_docproc.py +56 -0
- devsper/tools/documents/document_to_markdown.py +29 -0
- devsper/tools/documents/extract_document_images.py +39 -0
- devsper/tools/documents/extract_document_text.py +29 -0
- devsper/tools/documents/extract_equations.py +36 -0
- devsper/tools/documents/extract_tables.py +47 -0
- devsper/tools/documents/summarize_document.py +42 -0
- devsper/tools/documents/write_latex_document.py +133 -0
- devsper/tools/documents/write_markdown_document.py +89 -0
- devsper/tools/documents/write_word_document.py +149 -0
- devsper/tools/experiments/__init__.py +13 -0
- devsper/tools/experiments/bootstrap_estimator.py +54 -0
- devsper/tools/experiments/experiment_report_generator.py +50 -0
- devsper/tools/experiments/experiment_tracker.py +36 -0
- devsper/tools/experiments/grid_search_runner.py +50 -0
- devsper/tools/experiments/model_benchmark_runner.py +45 -0
- devsper/tools/experiments/monte_carlo_experiment.py +38 -0
- devsper/tools/experiments/parameter_sweep_runner.py +51 -0
- devsper/tools/experiments/result_comparator.py +58 -0
- devsper/tools/experiments/simulation_runner.py +43 -0
- devsper/tools/experiments/statistical_significance_test.py +56 -0
- devsper/tools/experiments/swarm_map_reduce.py +42 -0
- devsper/tools/filesystem/__init__.py +12 -0
- devsper/tools/filesystem/append_file.py +42 -0
- devsper/tools/filesystem/file_hash.py +40 -0
- devsper/tools/filesystem/file_line_count.py +36 -0
- devsper/tools/filesystem/file_metadata.py +38 -0
- devsper/tools/filesystem/file_preview.py +55 -0
- devsper/tools/filesystem/find_large_files.py +50 -0
- devsper/tools/filesystem/list_directory.py +39 -0
- devsper/tools/filesystem/read_file.py +35 -0
- devsper/tools/filesystem/search_files.py +60 -0
- devsper/tools/filesystem/write_file.py +41 -0
- devsper/tools/flagship/__init__.py +15 -0
- devsper/tools/flagship/distributed_document_analysis.py +77 -0
- devsper/tools/flagship/docproc_corpus_pipeline.py +91 -0
- devsper/tools/flagship/repository_semantic_map.py +99 -0
- devsper/tools/flagship/research_graph_builder.py +111 -0
- devsper/tools/flagship/swarm_experiment_runner.py +86 -0
- devsper/tools/knowledge/__init__.py +10 -0
- devsper/tools/knowledge/citation_graph_builder.py +69 -0
- devsper/tools/knowledge/concept_frequency_analyzer.py +74 -0
- devsper/tools/knowledge/corpus_builder.py +66 -0
- devsper/tools/knowledge/cross_document_entity_linker.py +71 -0
- devsper/tools/knowledge/document_corpus_summary.py +68 -0
- devsper/tools/knowledge/document_topic_extractor.py +58 -0
- devsper/tools/knowledge/knowledge_graph_extractor.py +58 -0
- devsper/tools/knowledge/timeline_extractor.py +59 -0
- devsper/tools/math/__init__.py +12 -0
- devsper/tools/math/calculate_expression.py +52 -0
- devsper/tools/math/correlation.py +44 -0
- devsper/tools/math/distribution_summary.py +39 -0
- devsper/tools/math/histogram.py +53 -0
- devsper/tools/math/linear_regression.py +47 -0
- devsper/tools/math/matrix_multiply.py +38 -0
- devsper/tools/math/mean_std.py +35 -0
- devsper/tools/math/monte_carlo_simulation.py +43 -0
- devsper/tools/math/polynomial_fit.py +40 -0
- devsper/tools/math/random_sample.py +36 -0
- devsper/tools/mcp/__init__.py +23 -0
- devsper/tools/mcp/adapter.py +53 -0
- devsper/tools/mcp/client.py +235 -0
- devsper/tools/mcp/discovery.py +53 -0
- devsper/tools/memory/__init__.py +16 -0
- devsper/tools/memory/delete_memory.py +25 -0
- devsper/tools/memory/list_memory.py +34 -0
- devsper/tools/memory/search_memory.py +36 -0
- devsper/tools/memory/store_memory.py +47 -0
- devsper/tools/memory/summarize_memory.py +41 -0
- devsper/tools/memory/tag_memory.py +47 -0
- devsper/tools/pipelines.py +92 -0
- devsper/tools/registry.py +39 -0
- devsper/tools/research/__init__.py +12 -0
- devsper/tools/research/arxiv_download.py +55 -0
- devsper/tools/research/arxiv_search.py +58 -0
- devsper/tools/research/citation_extractor.py +35 -0
- devsper/tools/research/duckduckgo_search.py +42 -0
- devsper/tools/research/paper_metadata_extractor.py +45 -0
- devsper/tools/research/paper_summarizer.py +41 -0
- devsper/tools/research/research_question_generator.py +39 -0
- devsper/tools/research/topic_cluster.py +46 -0
- devsper/tools/research/web_search.py +47 -0
- devsper/tools/research/wikipedia_lookup.py +50 -0
- devsper/tools/research_advanced/__init__.py +14 -0
- devsper/tools/research_advanced/citation_context_extractor.py +60 -0
- devsper/tools/research_advanced/literature_review_generator.py +79 -0
- devsper/tools/research_advanced/methodology_extractor.py +58 -0
- devsper/tools/research_advanced/paper_contribution_extractor.py +50 -0
- devsper/tools/research_advanced/paper_dataset_identifier.py +49 -0
- devsper/tools/research_advanced/paper_method_comparator.py +62 -0
- devsper/tools/research_advanced/paper_similarity_search.py +69 -0
- devsper/tools/research_advanced/paper_trend_analyzer.py +69 -0
- devsper/tools/research_advanced/parallel_document_analyzer.py +56 -0
- devsper/tools/research_advanced/research_gap_finder.py +71 -0
- devsper/tools/research_advanced/research_topic_mapper.py +69 -0
- devsper/tools/research_advanced/swarm_literature_review.py +58 -0
- devsper/tools/scoring/__init__.py +52 -0
- devsper/tools/scoring/report.py +44 -0
- devsper/tools/scoring/scorer.py +39 -0
- devsper/tools/scoring/selector.py +61 -0
- devsper/tools/scoring/store.py +267 -0
- devsper/tools/selector.py +130 -0
- devsper/tools/system/__init__.py +12 -0
- devsper/tools/system/cpu_usage.py +22 -0
- devsper/tools/system/disk_usage.py +35 -0
- devsper/tools/system/environment_variables.py +29 -0
- devsper/tools/system/memory_usage.py +23 -0
- devsper/tools/system/pip_install.py +44 -0
- devsper/tools/system/pip_search.py +29 -0
- devsper/tools/system/process_list.py +34 -0
- devsper/tools/system/python_package_list.py +40 -0
- devsper/tools/system/run_shell_command.py +51 -0
- devsper/tools/system/system_info.py +26 -0
- devsper/tools/tool_runner.py +122 -0
- devsper/tui/__init__.py +5 -0
- devsper/tui/activity_feed_view.py +73 -0
- devsper/tui/adaptive_tasks_view.py +75 -0
- devsper/tui/agent_role_view.py +35 -0
- devsper/tui/app.py +395 -0
- devsper/tui/dashboard_screen.py +290 -0
- devsper/tui/dev_view.py +99 -0
- devsper/tui/inject_screen.py +73 -0
- devsper/tui/knowledge_graph_view.py +46 -0
- devsper/tui/layout.py +43 -0
- devsper/tui/logs_view.py +83 -0
- devsper/tui/memory_view.py +58 -0
- devsper/tui/performance_view.py +33 -0
- devsper/tui/reasoning_graph_view.py +39 -0
- devsper/tui/results_view.py +139 -0
- devsper/tui/swarm_view.py +37 -0
- devsper/tui/task_detail_screen.py +55 -0
- devsper/tui/task_view.py +103 -0
- devsper/types/event.py +97 -0
- devsper/types/exceptions.py +21 -0
- devsper/types/swarm.py +41 -0
- devsper/types/task.py +80 -0
- devsper/upgrade/__init__.py +21 -0
- devsper/upgrade/changelog.py +124 -0
- devsper/upgrade/cli.py +145 -0
- devsper/upgrade/installer.py +103 -0
- devsper/upgrade/notifier.py +52 -0
- devsper/upgrade/version_check.py +121 -0
- devsper/utils/event_logger.py +88 -0
- devsper/utils/http.py +43 -0
- devsper/utils/models.py +54 -0
- devsper/visualization/__init__.py +5 -0
- devsper/visualization/dag_export.py +67 -0
- devsper/workflow/__init__.py +18 -0
- devsper/workflow/conditions.py +157 -0
- devsper/workflow/context.py +108 -0
- devsper/workflow/loader.py +156 -0
- devsper/workflow/resolver.py +109 -0
- devsper/workflow/runner.py +562 -0
- devsper/workflow/schema.py +63 -0
- devsper/workflow/validator.py +128 -0
- devsper-2.1.6.dist-info/METADATA +346 -0
- devsper-2.1.6.dist-info/RECORD +375 -0
- devsper-2.1.6.dist-info/WHEEL +4 -0
- devsper-2.1.6.dist-info/entry_points.txt +3 -0
- devsper-2.1.6.dist-info/licenses/LICENSE +639 -0
devsper/bus/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Message bus: real pub/sub for task and agent events."""
|
|
2
|
+
|
|
3
|
+
from devsper.bus.message import BusMessage, create_bus_message
|
|
4
|
+
from devsper.bus.backends.base import BusBackend
|
|
5
|
+
from devsper.bus.backends.memory import InMemoryBus
|
|
6
|
+
from devsper.bus.backends.redis import RedisBus
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_bus(config: object) -> BusBackend:
|
|
10
|
+
"""Return bus backend from config. Default InMemoryBus if bus config missing."""
|
|
11
|
+
backend = getattr(getattr(config, "bus", None), "backend", "memory")
|
|
12
|
+
if backend == "redis":
|
|
13
|
+
redis_url = getattr(getattr(config, "bus", None), "redis_url", "redis://localhost:6379")
|
|
14
|
+
return RedisBus(redis_url=redis_url)
|
|
15
|
+
return InMemoryBus()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"get_bus",
|
|
20
|
+
"MessageBus",
|
|
21
|
+
"BusMessage",
|
|
22
|
+
"BusBackend",
|
|
23
|
+
"InMemoryBus",
|
|
24
|
+
"RedisBus",
|
|
25
|
+
"create_bus_message",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
# Alias for spec
|
|
29
|
+
MessageBus = BusBackend
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Abstract base for message bus backends."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Awaitable, Callable
|
|
5
|
+
|
|
6
|
+
from devsper.bus.message import BusMessage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BusBackend(ABC):
|
|
10
|
+
@abstractmethod
|
|
11
|
+
async def publish(self, message: BusMessage) -> None:
|
|
12
|
+
"""Publish a message to the bus."""
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
async def subscribe(
|
|
17
|
+
self,
|
|
18
|
+
topic: str,
|
|
19
|
+
handler: Callable[[BusMessage], Awaitable[None]],
|
|
20
|
+
run_id: str | None = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Subscribe to a topic (supports wildcards like task.*). run_id scopes channel when set (Redis)."""
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
async def unsubscribe(self, topic: str) -> None:
|
|
27
|
+
"""Unsubscribe from a topic."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
async def start(self) -> None:
|
|
32
|
+
"""Start the backend (e.g. connect to Redis)."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
async def stop(self) -> None:
|
|
37
|
+
"""Stop the backend."""
|
|
38
|
+
...
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""In-memory asyncio bus backend. Single-node, zero config."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import fnmatch
|
|
5
|
+
from typing import Awaitable, Callable
|
|
6
|
+
|
|
7
|
+
from devsper.bus.message import BusMessage
|
|
8
|
+
from devsper.bus.backends.base import BusBackend
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _topic_matches(pattern: str, topic: str) -> bool:
|
|
12
|
+
"""Return True if topic matches pattern (supports * wildcard)."""
|
|
13
|
+
if pattern == topic:
|
|
14
|
+
return True
|
|
15
|
+
if "*" in pattern:
|
|
16
|
+
return fnmatch.fnmatch(topic, pattern)
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InMemoryBus(BusBackend):
|
|
21
|
+
"""Asyncio-based in-memory bus. Dict of topic pattern -> list of handler coroutines."""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._handlers: dict[str, list[Callable[[BusMessage], Awaitable[None]]]] = {}
|
|
25
|
+
self._started = False
|
|
26
|
+
|
|
27
|
+
async def start(self) -> None:
|
|
28
|
+
self._started = True
|
|
29
|
+
|
|
30
|
+
async def stop(self) -> None:
|
|
31
|
+
self._started = False
|
|
32
|
+
self._handlers.clear()
|
|
33
|
+
|
|
34
|
+
async def publish(self, message: BusMessage) -> None:
|
|
35
|
+
topic = message.topic
|
|
36
|
+
to_call: list[Awaitable[None]] = []
|
|
37
|
+
for pattern, handlers in self._handlers.items():
|
|
38
|
+
if _topic_matches(pattern, topic):
|
|
39
|
+
for h in handlers:
|
|
40
|
+
to_call.append(h(message))
|
|
41
|
+
if to_call:
|
|
42
|
+
await asyncio.gather(*to_call)
|
|
43
|
+
|
|
44
|
+
async def subscribe(
|
|
45
|
+
self,
|
|
46
|
+
topic: str,
|
|
47
|
+
handler: Callable[[BusMessage], Awaitable[None]],
|
|
48
|
+
run_id: str | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
if topic not in self._handlers:
|
|
51
|
+
self._handlers[topic] = []
|
|
52
|
+
self._handlers[topic].append(handler)
|
|
53
|
+
|
|
54
|
+
async def unsubscribe(self, topic: str) -> None:
|
|
55
|
+
self._handlers.pop(topic, None)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Redis pub/sub bus backend."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Awaitable, Callable
|
|
6
|
+
|
|
7
|
+
from devsper.bus.message import BusMessage
|
|
8
|
+
from devsper.bus.backends.base import BusBackend
|
|
9
|
+
from devsper.types.exceptions import BusConnectionError
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RedisBus(BusBackend):
|
|
15
|
+
"""Redis pub/sub backend. Lazy import redis.asyncio."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, redis_url: str = "redis://localhost:6379") -> None:
|
|
18
|
+
self._redis_url = redis_url
|
|
19
|
+
self._pub: object = None
|
|
20
|
+
self._sub: object = None
|
|
21
|
+
self._pubsub: object = None
|
|
22
|
+
self._handlers: dict[str, list[Callable[[BusMessage], Awaitable[None]]]] = {}
|
|
23
|
+
self._listen_task: asyncio.Task | None = None
|
|
24
|
+
self._running = False
|
|
25
|
+
|
|
26
|
+
async def start(self) -> None:
|
|
27
|
+
try:
|
|
28
|
+
import redis.asyncio as aioredis
|
|
29
|
+
except ImportError as e:
|
|
30
|
+
raise ImportError(
|
|
31
|
+
"Redis bus requires redis package. Install with: pip install redis"
|
|
32
|
+
) from e
|
|
33
|
+
try:
|
|
34
|
+
self._pub = aioredis.from_url(self._redis_url)
|
|
35
|
+
self._sub = aioredis.from_url(self._redis_url)
|
|
36
|
+
self._pubsub = self._sub.pubsub()
|
|
37
|
+
await self._pub.ping()
|
|
38
|
+
except Exception as e:
|
|
39
|
+
raise BusConnectionError(
|
|
40
|
+
f"Cannot connect to Redis at {self._redis_url}: {e}"
|
|
41
|
+
) from e
|
|
42
|
+
self._running = True
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def redis_client(self) -> object:
|
|
46
|
+
"""Redis connection for cluster registry/election/state (v1.10)."""
|
|
47
|
+
return self._pub
|
|
48
|
+
|
|
49
|
+
async def stop(self) -> None:
|
|
50
|
+
self._running = False
|
|
51
|
+
if self._listen_task is not None:
|
|
52
|
+
self._listen_task.cancel()
|
|
53
|
+
try:
|
|
54
|
+
await self._listen_task
|
|
55
|
+
except asyncio.CancelledError:
|
|
56
|
+
pass
|
|
57
|
+
self._listen_task = None
|
|
58
|
+
if self._pubsub is not None:
|
|
59
|
+
try:
|
|
60
|
+
await self._pubsub.aclose()
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
self._pubsub = None
|
|
64
|
+
if self._pub is not None:
|
|
65
|
+
try:
|
|
66
|
+
await self._pub.aclose()
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
self._pub = None
|
|
70
|
+
if self._sub is not None:
|
|
71
|
+
try:
|
|
72
|
+
await self._sub.aclose()
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
self._sub = None
|
|
76
|
+
self._handlers.clear()
|
|
77
|
+
|
|
78
|
+
def _channel(self, topic: str, run_id: str | None = None) -> str:
|
|
79
|
+
"""Redis channel: topic:run_id when run_id is set, else topic (all runs)."""
|
|
80
|
+
if run_id and run_id.strip():
|
|
81
|
+
return f"{topic}:{run_id}"
|
|
82
|
+
return topic
|
|
83
|
+
|
|
84
|
+
async def publish(self, message: BusMessage) -> None:
|
|
85
|
+
if self._pub is None:
|
|
86
|
+
raise BusConnectionError("Redis bus not started. Call start() first.")
|
|
87
|
+
channel = self._channel(message.topic, getattr(message, "run_id", None))
|
|
88
|
+
await self._pub.publish(channel, message.to_json())
|
|
89
|
+
|
|
90
|
+
async def subscribe(
|
|
91
|
+
self,
|
|
92
|
+
topic: str,
|
|
93
|
+
handler: Callable[[BusMessage], Awaitable[None]],
|
|
94
|
+
run_id: str | None = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
if self._pubsub is None:
|
|
97
|
+
raise BusConnectionError("Redis bus not started. Call start() first.")
|
|
98
|
+
channel = self._channel(topic, run_id)
|
|
99
|
+
if channel not in self._handlers:
|
|
100
|
+
self._handlers[channel] = []
|
|
101
|
+
await self._pubsub.subscribe(channel)
|
|
102
|
+
self._handlers[channel].append(handler)
|
|
103
|
+
if self._listen_task is None or self._listen_task.done():
|
|
104
|
+
self._listen_task = asyncio.create_task(self._listen())
|
|
105
|
+
|
|
106
|
+
async def _listen(self) -> None:
|
|
107
|
+
while self._running and self._pubsub is not None:
|
|
108
|
+
try:
|
|
109
|
+
async for raw in self._pubsub.listen():
|
|
110
|
+
if not self._running:
|
|
111
|
+
break
|
|
112
|
+
if raw.get("type") == "message":
|
|
113
|
+
channel = raw.get("channel")
|
|
114
|
+
if isinstance(channel, bytes):
|
|
115
|
+
channel = channel.decode("utf-8")
|
|
116
|
+
data = raw.get("data")
|
|
117
|
+
if data is None:
|
|
118
|
+
continue
|
|
119
|
+
payload = data.decode("utf-8") if isinstance(data, bytes) else data
|
|
120
|
+
try:
|
|
121
|
+
msg = BusMessage.from_json(payload)
|
|
122
|
+
for h in self._handlers.get(channel, []):
|
|
123
|
+
task = asyncio.create_task(h(msg))
|
|
124
|
+
|
|
125
|
+
def _done(t):
|
|
126
|
+
try:
|
|
127
|
+
t.result()
|
|
128
|
+
except asyncio.CancelledError:
|
|
129
|
+
pass
|
|
130
|
+
except Exception as e:
|
|
131
|
+
log.warning("Bus handler failed: %s", e, exc_info=True)
|
|
132
|
+
|
|
133
|
+
task.add_done_callback(_done)
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
except asyncio.CancelledError:
|
|
137
|
+
break
|
|
138
|
+
except Exception:
|
|
139
|
+
if self._running:
|
|
140
|
+
await asyncio.sleep(0.5)
|
|
141
|
+
|
|
142
|
+
async def unsubscribe(self, topic: str, run_id: str | None = None) -> None:
|
|
143
|
+
channel = self._channel(topic, run_id)
|
|
144
|
+
if self._pubsub is not None and channel in self._handlers:
|
|
145
|
+
await self._pubsub.unsubscribe(channel)
|
|
146
|
+
self._handlers.pop(channel, None)
|
devsper/bus/message.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Bus message: serializable pub/sub payload."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class BusMessage:
|
|
11
|
+
id: str
|
|
12
|
+
topic: str
|
|
13
|
+
payload: dict
|
|
14
|
+
sender_id: str
|
|
15
|
+
timestamp: str
|
|
16
|
+
run_id: str
|
|
17
|
+
|
|
18
|
+
def to_json(self) -> str:
|
|
19
|
+
return json.dumps({
|
|
20
|
+
"id": self.id,
|
|
21
|
+
"topic": self.topic,
|
|
22
|
+
"payload": self.payload,
|
|
23
|
+
"sender_id": self.sender_id,
|
|
24
|
+
"timestamp": self.timestamp,
|
|
25
|
+
"run_id": self.run_id,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_json(cls, raw: str) -> "BusMessage":
|
|
30
|
+
data = json.loads(raw)
|
|
31
|
+
return cls(
|
|
32
|
+
id=data["id"],
|
|
33
|
+
topic=data["topic"],
|
|
34
|
+
payload=dict(data.get("payload", {})),
|
|
35
|
+
sender_id=data.get("sender_id", "local"),
|
|
36
|
+
timestamp=data.get("timestamp", datetime.now(timezone.utc).isoformat()),
|
|
37
|
+
run_id=data.get("run_id", ""),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def create_bus_message(
|
|
42
|
+
topic: str,
|
|
43
|
+
payload: dict,
|
|
44
|
+
*,
|
|
45
|
+
sender_id: str = "local",
|
|
46
|
+
run_id: str = "",
|
|
47
|
+
) -> BusMessage:
|
|
48
|
+
"""Create a BusMessage with generated id and current timestamp."""
|
|
49
|
+
return BusMessage(
|
|
50
|
+
id=str(uuid4()),
|
|
51
|
+
topic=topic,
|
|
52
|
+
payload=payload,
|
|
53
|
+
sender_id=sender_id,
|
|
54
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
55
|
+
run_id=run_id,
|
|
56
|
+
)
|
devsper/bus/topics.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Bus topic constants for pub/sub."""
|
|
2
|
+
|
|
3
|
+
TASK_READY = "task.ready"
|
|
4
|
+
TASK_STARTED = "task.started"
|
|
5
|
+
TASK_COMPLETED = "task.completed"
|
|
6
|
+
TASK_FAILED = "task.failed"
|
|
7
|
+
TASK_CLAIMED = "task.claimed"
|
|
8
|
+
TASK_CLAIM_GRANTED = "task.claim_granted"
|
|
9
|
+
TASK_CLAIM_REJECTED = "task.claim_rejected"
|
|
10
|
+
AGENT_BROADCAST = "agent.broadcast"
|
|
11
|
+
SWARM_CONTROL = "swarm.control"
|
|
12
|
+
NODE_HEARTBEAT = "node.heartbeat"
|
|
13
|
+
NODE_JOINED = "node.joined"
|
|
14
|
+
NODE_LEFT = "node.left"
|
|
15
|
+
NODE_BECAME_LEADER = "node.became_leader"
|
|
16
|
+
NODE_LOST_LEADERSHIP = "node.lost_leadership"
|
|
17
|
+
SWARM_SNAPSHOT = "swarm.snapshot"
|
|
18
|
+
SWARM_STATUS_REQUEST = "swarm.status_request"
|
|
19
|
+
SWARM_STATUS_RESPONSE = "swarm.status_response"
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Task result cache: hash task identity and store/retrieve results."""
|
|
2
|
+
|
|
3
|
+
from devsper.cache.hashing import task_hash
|
|
4
|
+
from devsper.cache.task_cache import TaskCache, SemanticTaskCache, CacheHit
|
|
5
|
+
|
|
6
|
+
__all__ = ["task_hash", "TaskCache", "SemanticTaskCache", "CacheHit"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
In-memory embedding index for semantic cache lookup. Uses same embedding provider as memory.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from devsper.memory.embeddings import embed_text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _cosine_sim(a: list[float], b: list[float]) -> float:
|
|
14
|
+
if not a or not b or len(a) != len(b):
|
|
15
|
+
return 0.0
|
|
16
|
+
dot = sum(x * y for x, y in zip(a, b))
|
|
17
|
+
na = sum(x * x for x in a) ** 0.5
|
|
18
|
+
nb = sum(x * x for x in b) ** 0.5
|
|
19
|
+
if na == 0 or nb == 0:
|
|
20
|
+
return 0.0
|
|
21
|
+
return dot / (na * nb)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _embedding_to_bytes(vec: list[float]) -> bytes:
|
|
25
|
+
arr = np.array(vec, dtype=np.float64)
|
|
26
|
+
buf = io.BytesIO()
|
|
27
|
+
np.save(buf, arr, allow_pickle=False)
|
|
28
|
+
return buf.getvalue()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _bytes_to_embedding(data: bytes) -> list[float]:
|
|
32
|
+
buf = io.BytesIO(data)
|
|
33
|
+
arr = np.load(buf, allow_pickle=False)
|
|
34
|
+
return arr.tolist()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CacheEmbeddingIndex:
|
|
38
|
+
"""
|
|
39
|
+
Same embedding provider as memory (embed_text). Loads entries from store,
|
|
40
|
+
keeps vectors in memory for nearest-neighbor search.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, store: Any = None) -> None:
|
|
44
|
+
self.store = store
|
|
45
|
+
self._vectors: list[list[float]] = []
|
|
46
|
+
self._meta: list[tuple[str, str, str, float, str]] = [] # key, result, task_type, created_at, original_description
|
|
47
|
+
|
|
48
|
+
def rebuild(self) -> None:
|
|
49
|
+
"""Load all semantic entries from store and index by embedding."""
|
|
50
|
+
self._vectors = []
|
|
51
|
+
self._meta = []
|
|
52
|
+
if not self.store:
|
|
53
|
+
return
|
|
54
|
+
for emb_blob, result, task_type, created_at, key, original_desc in self.store.list_semantic_entries():
|
|
55
|
+
vec = _bytes_to_embedding(emb_blob)
|
|
56
|
+
self._vectors.append(vec)
|
|
57
|
+
self._meta.append((key, result, task_type, created_at, original_desc))
|
|
58
|
+
|
|
59
|
+
def add(self, embedding: list[float], key: str, result: str, task_type: str, created_at: float, original_description: str) -> None:
|
|
60
|
+
self._vectors.append(embedding)
|
|
61
|
+
self._meta.append((key, result, task_type, created_at, original_description))
|
|
62
|
+
|
|
63
|
+
def search(
|
|
64
|
+
self,
|
|
65
|
+
query_embedding: list[float],
|
|
66
|
+
threshold: float,
|
|
67
|
+
max_age_seconds: float,
|
|
68
|
+
) -> tuple[float, str, str, str, str, str] | None:
|
|
69
|
+
"""
|
|
70
|
+
Nearest neighbor search. Returns (similarity, result, original_description, cached_at, task_type, key)
|
|
71
|
+
for best match >= threshold and not expired, or None.
|
|
72
|
+
"""
|
|
73
|
+
import time
|
|
74
|
+
now = time.time()
|
|
75
|
+
best_sim = -1.0
|
|
76
|
+
best = None
|
|
77
|
+
for i, vec in enumerate(self._vectors):
|
|
78
|
+
sim = _cosine_sim(query_embedding, vec)
|
|
79
|
+
if sim < threshold:
|
|
80
|
+
continue
|
|
81
|
+
key, result, task_type, created_at, original_desc = self._meta[i]
|
|
82
|
+
if max_age_seconds > 0 and (now - created_at) > max_age_seconds:
|
|
83
|
+
continue
|
|
84
|
+
if sim > best_sim:
|
|
85
|
+
best_sim = sim
|
|
86
|
+
best = (result, original_desc, created_at, task_type, key)
|
|
87
|
+
if best is None:
|
|
88
|
+
return None
|
|
89
|
+
result, original_desc, created_at, task_type, key = best
|
|
90
|
+
return (best_sim, result, original_desc, str(created_at), task_type, key)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def embedding_to_bytes(vec: list[float]) -> bytes:
|
|
94
|
+
return _embedding_to_bytes(vec)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def bytes_to_embedding(data: bytes) -> list[float]:
|
|
98
|
+
return _bytes_to_embedding(data)
|
devsper/cache/hashing.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Hash task identity for cache keys: description, dependencies, and optional tool usage."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def task_hash(
|
|
8
|
+
task_id: str,
|
|
9
|
+
description: str,
|
|
10
|
+
dependencies: list[str],
|
|
11
|
+
tool_usage: str | None = None,
|
|
12
|
+
) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Produce a stable hash for a task. Used for cache lookup.
|
|
15
|
+
Includes: task id, description, dependencies, and optional tool_usage signature.
|
|
16
|
+
"""
|
|
17
|
+
payload = {
|
|
18
|
+
"id": task_id,
|
|
19
|
+
"description": (description or "").strip(),
|
|
20
|
+
"dependencies": sorted(dependencies) if dependencies else [],
|
|
21
|
+
"tool_usage": tool_usage or "",
|
|
22
|
+
}
|
|
23
|
+
blob = json.dumps(payload, sort_keys=True)
|
|
24
|
+
return hashlib.sha256(blob.encode("utf-8")).hexdigest()
|
devsper/cache/store.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cache store backend for task cache. Holds exact-match and semantic (embedding) entries.
|
|
3
|
+
SQLite with optional BLOB column for embeddings; semantic entries use a separate table.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sqlite3
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Protocol
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CacheStore(Protocol):
|
|
13
|
+
"""Protocol for cache storage (exact key or semantic)."""
|
|
14
|
+
|
|
15
|
+
def get(self, key: str) -> tuple[str, float] | None:
|
|
16
|
+
"""Return (result, created_at) for key or None."""
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
def set(self, key: str, result: str, created_at: float | None = None) -> None:
|
|
20
|
+
"""Store result for key."""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
def list_semantic_entries(self) -> list[tuple[bytes, str, str, float, str, str]]:
|
|
24
|
+
"""Return list of (embedding_blob, result, task_type, created_at, key, original_description)."""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
def put_semantic(
|
|
28
|
+
self,
|
|
29
|
+
key: str,
|
|
30
|
+
embedding_blob: bytes,
|
|
31
|
+
result: str,
|
|
32
|
+
task_type: str,
|
|
33
|
+
original_description: str,
|
|
34
|
+
created_at: float | None = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Store a semantic cache entry."""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
def delete(self, key: str) -> None:
|
|
40
|
+
"""Remove entry by key."""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
def clear(self) -> None:
|
|
44
|
+
"""Remove all entries."""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
def stats(self) -> dict:
|
|
48
|
+
"""Return counts and optional semantic stats."""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _default_db_path() -> Path:
|
|
53
|
+
return Path(".devsper") / "task_cache.db"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DefaultCacheStore:
|
|
57
|
+
"""
|
|
58
|
+
SQLite-backed store: exact-match in task_cache; semantic in semantic_cache table.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, db_path: str | Path | None = None) -> None:
|
|
62
|
+
self.db_path = Path(db_path) if db_path else _default_db_path()
|
|
63
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
self._init_schema()
|
|
65
|
+
|
|
66
|
+
def _conn(self) -> sqlite3.Connection:
|
|
67
|
+
return sqlite3.connect(str(self.db_path))
|
|
68
|
+
|
|
69
|
+
def _init_schema(self) -> None:
|
|
70
|
+
with self._conn() as c:
|
|
71
|
+
c.execute("""
|
|
72
|
+
CREATE TABLE IF NOT EXISTS task_cache (
|
|
73
|
+
key TEXT PRIMARY KEY,
|
|
74
|
+
result TEXT NOT NULL,
|
|
75
|
+
created_at REAL
|
|
76
|
+
)
|
|
77
|
+
""")
|
|
78
|
+
c.execute("""
|
|
79
|
+
CREATE TABLE IF NOT EXISTS semantic_cache (
|
|
80
|
+
key TEXT PRIMARY KEY,
|
|
81
|
+
embedding BLOB NOT NULL,
|
|
82
|
+
result TEXT NOT NULL,
|
|
83
|
+
task_type TEXT NOT NULL,
|
|
84
|
+
created_at REAL NOT NULL,
|
|
85
|
+
original_description TEXT NOT NULL
|
|
86
|
+
)
|
|
87
|
+
""")
|
|
88
|
+
c.execute(
|
|
89
|
+
"CREATE INDEX IF NOT EXISTS idx_semantic_task_type ON semantic_cache(task_type)"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def get(self, key: str) -> tuple[str, float] | None:
|
|
93
|
+
with self._conn() as c:
|
|
94
|
+
row = c.execute(
|
|
95
|
+
"SELECT result, created_at FROM task_cache WHERE key = ?", (key,)
|
|
96
|
+
).fetchone()
|
|
97
|
+
return (row[0], row[1]) if row else None
|
|
98
|
+
|
|
99
|
+
def set(self, key: str, result: str, created_at: float | None = None) -> None:
|
|
100
|
+
ts = created_at if created_at is not None else time.time()
|
|
101
|
+
with self._conn() as c:
|
|
102
|
+
c.execute(
|
|
103
|
+
"INSERT OR REPLACE INTO task_cache (key, result, created_at) VALUES (?, ?, ?)",
|
|
104
|
+
(key, result, ts),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def list_semantic_entries(
|
|
108
|
+
self,
|
|
109
|
+
) -> list[tuple[bytes, str, str, float, str, str]]:
|
|
110
|
+
with self._conn() as c:
|
|
111
|
+
rows = c.execute(
|
|
112
|
+
"SELECT embedding, result, task_type, created_at, key, original_description FROM semantic_cache"
|
|
113
|
+
).fetchall()
|
|
114
|
+
return list(rows)
|
|
115
|
+
|
|
116
|
+
def put_semantic(
|
|
117
|
+
self,
|
|
118
|
+
key: str,
|
|
119
|
+
embedding_blob: bytes,
|
|
120
|
+
result: str,
|
|
121
|
+
task_type: str,
|
|
122
|
+
original_description: str,
|
|
123
|
+
created_at: float | None = None,
|
|
124
|
+
) -> None:
|
|
125
|
+
ts = created_at if created_at is not None else time.time()
|
|
126
|
+
with self._conn() as c:
|
|
127
|
+
c.execute(
|
|
128
|
+
"""INSERT OR REPLACE INTO semantic_cache
|
|
129
|
+
(key, embedding, result, task_type, created_at, original_description)
|
|
130
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
131
|
+
(key, embedding_blob, result, task_type, ts, original_description),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def delete(self, key: str) -> None:
|
|
135
|
+
with self._conn() as c:
|
|
136
|
+
c.execute("DELETE FROM task_cache WHERE key = ?", (key,))
|
|
137
|
+
c.execute("DELETE FROM semantic_cache WHERE key = ?", (key,))
|
|
138
|
+
|
|
139
|
+
def clear(self) -> None:
|
|
140
|
+
with self._conn() as c:
|
|
141
|
+
c.execute("DELETE FROM task_cache")
|
|
142
|
+
c.execute("DELETE FROM semantic_cache")
|
|
143
|
+
|
|
144
|
+
def stats(self) -> dict:
|
|
145
|
+
with self._conn() as c:
|
|
146
|
+
exact = c.execute("SELECT COUNT(*) FROM task_cache").fetchone()[0]
|
|
147
|
+
semantic = c.execute("SELECT COUNT(*) FROM semantic_cache").fetchone()[0]
|
|
148
|
+
return {"entries": exact, "semantic_entries": semantic }
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_default_cache_store(db_path: str | Path | None = None) -> DefaultCacheStore:
|
|
152
|
+
"""Return the default cache store (same DB path as TaskCache)."""
|
|
153
|
+
return DefaultCacheStore(db_path=db_path)
|