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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lightweight embeddings for memory index.
|
|
3
|
+
|
|
4
|
+
Uses optional OpenAI embeddings when OPENAI_API_KEY is set;
|
|
5
|
+
otherwise a deterministic stub for tests and offline use.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
DEFAULT_DIM = 64
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _stub_embed(text: str, dim: int = DEFAULT_DIM) -> list[float]:
|
|
16
|
+
"""Deterministic pseudo-embedding from text (no API). Normalized to unit-ish scale."""
|
|
17
|
+
words = re.findall(r"\w+", text.lower())
|
|
18
|
+
if not words:
|
|
19
|
+
return [0.0] * dim
|
|
20
|
+
vec = [0.0] * dim
|
|
21
|
+
for w in words:
|
|
22
|
+
h = hashlib.sha256(w.encode()).hexdigest()
|
|
23
|
+
for i in range(0, min(dim * 2, len(h)), 2):
|
|
24
|
+
vec[i % dim] += int(h[i : i + 2], 16) / 255.0
|
|
25
|
+
total = sum(x * x for x in vec) ** 0.5
|
|
26
|
+
if total > 0:
|
|
27
|
+
vec = [x / total for x in vec]
|
|
28
|
+
return vec
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _openai_embed(text: str, dim: int = DEFAULT_DIM) -> list[float] | None:
|
|
32
|
+
"""Use OpenAI embeddings if available. Returns None if not configured."""
|
|
33
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
|
34
|
+
if not api_key or not text.strip():
|
|
35
|
+
return None
|
|
36
|
+
try:
|
|
37
|
+
from openai import OpenAI
|
|
38
|
+
|
|
39
|
+
client = OpenAI(api_key=api_key)
|
|
40
|
+
r = client.embeddings.create(
|
|
41
|
+
input=text[:8192],
|
|
42
|
+
model="text-embedding-3-small",
|
|
43
|
+
)
|
|
44
|
+
emb = r.data[0].embedding
|
|
45
|
+
if dim and len(emb) > dim:
|
|
46
|
+
return emb[:dim]
|
|
47
|
+
return emb
|
|
48
|
+
except Exception:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def embed_text(text: str, dim: int = DEFAULT_DIM) -> list[float]:
|
|
53
|
+
"""
|
|
54
|
+
Return an embedding vector for text.
|
|
55
|
+
Uses OpenAI when OPENAI_API_KEY is set; otherwise a deterministic stub.
|
|
56
|
+
"""
|
|
57
|
+
if not text or not text.strip():
|
|
58
|
+
return _stub_embed(" ", dim)
|
|
59
|
+
out = _openai_embed(text, dim)
|
|
60
|
+
return out if out is not None else _stub_embed(text, dim)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Semantic search across stored memory via embeddings and top_k retrieval.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from devsper.memory.embeddings import embed_text
|
|
6
|
+
from devsper.memory.memory_store import MemoryStore
|
|
7
|
+
from devsper.memory.memory_types import MemoryRecord
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _cosine_sim(a: list[float], b: list[float]) -> float:
|
|
11
|
+
if not a or not b or len(a) != len(b):
|
|
12
|
+
return 0.0
|
|
13
|
+
dot = sum(x * y for x, y in zip(a, b))
|
|
14
|
+
na = sum(x * x for x in a) ** 0.5
|
|
15
|
+
nb = sum(x * x for x in b) ** 0.5
|
|
16
|
+
if na == 0 or nb == 0:
|
|
17
|
+
return 0.0
|
|
18
|
+
return dot / (na * nb)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MemoryIndex:
|
|
22
|
+
"""
|
|
23
|
+
Vector search over memory. Uses store for persistence and optional
|
|
24
|
+
embeddings on records for query_memory(text, top_k).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, store: MemoryStore | None = None) -> None:
|
|
28
|
+
self.store = store or MemoryStore()
|
|
29
|
+
|
|
30
|
+
def query_memory(
|
|
31
|
+
self,
|
|
32
|
+
text: str,
|
|
33
|
+
top_k: int = 5,
|
|
34
|
+
min_similarity: float = 0.0,
|
|
35
|
+
include_archived: bool = False,
|
|
36
|
+
) -> list[MemoryRecord]:
|
|
37
|
+
"""
|
|
38
|
+
Semantic search: embed query, score against stored records with embeddings,
|
|
39
|
+
return top_k by similarity above min_similarity. Records without embeddings
|
|
40
|
+
are skipped for ranking; if none have embeddings, return latest by timestamp.
|
|
41
|
+
Use min_similarity > 0 (e.g. 0.45) to avoid injecting barely-related memory.
|
|
42
|
+
By default excludes archived records (consolidation).
|
|
43
|
+
"""
|
|
44
|
+
records = self.store.list_memory(limit=500, include_archived=include_archived)
|
|
45
|
+
if not records:
|
|
46
|
+
return []
|
|
47
|
+
query_emb = embed_text(text)
|
|
48
|
+
with_emb = [r for r in records if r.embedding is not None]
|
|
49
|
+
if not with_emb:
|
|
50
|
+
return records[:top_k]
|
|
51
|
+
scored = [
|
|
52
|
+
(_cosine_sim(query_emb, r.embedding), r)
|
|
53
|
+
for r in with_emb
|
|
54
|
+
]
|
|
55
|
+
scored.sort(key=lambda x: -x[0])
|
|
56
|
+
if min_similarity > 0:
|
|
57
|
+
scored = [(s, r) for s, r in scored if s >= min_similarity]
|
|
58
|
+
return [r for _, r in scored[:top_k]]
|
|
59
|
+
|
|
60
|
+
def query_across_runs(
|
|
61
|
+
self,
|
|
62
|
+
text: str,
|
|
63
|
+
top_k: int = 20,
|
|
64
|
+
min_similarity: float = 0.0,
|
|
65
|
+
run_id_filter: str | None = None,
|
|
66
|
+
include_archived: bool = False,
|
|
67
|
+
) -> list[MemoryRecord]:
|
|
68
|
+
"""
|
|
69
|
+
v1.8: Same as query_memory but over more records (all runs), optional run_id filter.
|
|
70
|
+
Used by CrossRunSynthesizer. Excludes archived by default.
|
|
71
|
+
"""
|
|
72
|
+
records = self.store.list_memory(
|
|
73
|
+
limit=2000,
|
|
74
|
+
include_archived=include_archived,
|
|
75
|
+
run_id_filter=run_id_filter,
|
|
76
|
+
)
|
|
77
|
+
if not records:
|
|
78
|
+
return []
|
|
79
|
+
query_emb = embed_text(text)
|
|
80
|
+
with_emb = [r for r in records if r.embedding is not None]
|
|
81
|
+
if not with_emb:
|
|
82
|
+
return records[:top_k]
|
|
83
|
+
scored = [
|
|
84
|
+
(_cosine_sim(query_emb, r.embedding), r)
|
|
85
|
+
for r in with_emb
|
|
86
|
+
]
|
|
87
|
+
scored.sort(key=lambda x: -x[0])
|
|
88
|
+
if min_similarity > 0:
|
|
89
|
+
scored = [(s, r) for s, r in scored if s >= min_similarity]
|
|
90
|
+
return [r for _, r in scored[:top_k]]
|
|
91
|
+
|
|
92
|
+
def ensure_embedding(self, record: MemoryRecord) -> MemoryRecord:
|
|
93
|
+
"""Compute and attach embedding if missing; return record (unchanged if already set)."""
|
|
94
|
+
if record.embedding is not None:
|
|
95
|
+
return record
|
|
96
|
+
record.embedding = embed_text(record.content)
|
|
97
|
+
return record
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory router: determine which memories are relevant to a task and return context for the agent.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from devsper.memory.memory_index import MemoryIndex
|
|
6
|
+
from devsper.memory.memory_store import MemoryStore
|
|
7
|
+
from devsper.memory.memory_types import MemoryRecord
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MemoryRouter:
|
|
11
|
+
"""
|
|
12
|
+
Routes task descriptions to relevant memories (e.g. research, papers, codebase)
|
|
13
|
+
and formats them as context for the agent. Only memories above min_similarity
|
|
14
|
+
are included to avoid injecting off-topic context.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
store: MemoryStore | None = None,
|
|
20
|
+
index: MemoryIndex | None = None,
|
|
21
|
+
top_k: int = 10,
|
|
22
|
+
min_similarity: float = 0.55,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.store = store or MemoryStore()
|
|
25
|
+
self.index = index or MemoryIndex(self.store)
|
|
26
|
+
self.top_k = top_k
|
|
27
|
+
self.min_similarity = min_similarity
|
|
28
|
+
|
|
29
|
+
def get_relevant_memory(self, task: str) -> list[MemoryRecord]:
|
|
30
|
+
"""
|
|
31
|
+
Return memories relevant to the task (semantic search).
|
|
32
|
+
Only returns records with similarity >= min_similarity to avoid off-topic injection.
|
|
33
|
+
"""
|
|
34
|
+
return self.index.query_memory(
|
|
35
|
+
task,
|
|
36
|
+
top_k=self.top_k,
|
|
37
|
+
min_similarity=self.min_similarity,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def get_memory_context(self, task: str) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Format relevant memories as a string block for injection into the agent prompt.
|
|
43
|
+
User injections (tag user_injection) are always included first. Then semantic results.
|
|
44
|
+
Empty if no memories meet the relevance threshold.
|
|
45
|
+
"""
|
|
46
|
+
lines = []
|
|
47
|
+
inject_records = self.store.list_memory(tag_contains="user_injection", limit=10)
|
|
48
|
+
if inject_records:
|
|
49
|
+
lines.append("USER INJECTIONS (high priority):")
|
|
50
|
+
for r in inject_records:
|
|
51
|
+
lines.append(f"- {r.content[:1000]}{'...' if len(r.content) > 1000 else ''}")
|
|
52
|
+
records = self.get_relevant_memory(task)
|
|
53
|
+
if records:
|
|
54
|
+
if lines:
|
|
55
|
+
lines.append("")
|
|
56
|
+
lines.append("RELEVANT MEMORY (previous research notes, findings, artifacts):")
|
|
57
|
+
for r in records:
|
|
58
|
+
lines.append(
|
|
59
|
+
f"- [{r.memory_type.value}] {r.source_task or 'general'}: "
|
|
60
|
+
f"{r.content[:500]}{'...' if len(r.content) > 500 else ''}"
|
|
61
|
+
)
|
|
62
|
+
return "\n".join(lines) if lines else ""
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Persistent memory store: SQLite-backed store, retrieve, delete, list.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sqlite3
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from devsper.memory.memory_types import MemoryRecord, MemoryType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _default_db_path() -> str:
|
|
16
|
+
from devsper.config import get_config
|
|
17
|
+
|
|
18
|
+
base = os.environ.get("DEVSPER_DATA_DIR") or get_config().data_dir
|
|
19
|
+
os.makedirs(base, exist_ok=True)
|
|
20
|
+
return os.path.join(base, "memory.db")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MemoryStore:
|
|
24
|
+
"""Local persistent store for memory records. Uses SQLite."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, db_path: str | None = None) -> None:
|
|
27
|
+
self.db_path = db_path or _default_db_path()
|
|
28
|
+
Path(self.db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
self._init_schema()
|
|
30
|
+
|
|
31
|
+
def _conn(self) -> sqlite3.Connection:
|
|
32
|
+
return sqlite3.connect(self.db_path)
|
|
33
|
+
|
|
34
|
+
def _init_schema(self) -> None:
|
|
35
|
+
with self._conn() as conn:
|
|
36
|
+
conn.execute(
|
|
37
|
+
"""
|
|
38
|
+
CREATE TABLE IF NOT EXISTS memory (
|
|
39
|
+
memory_id TEXT PRIMARY KEY,
|
|
40
|
+
memory_type TEXT NOT NULL,
|
|
41
|
+
content TEXT NOT NULL,
|
|
42
|
+
tags TEXT,
|
|
43
|
+
timestamp TEXT NOT NULL,
|
|
44
|
+
source_task TEXT,
|
|
45
|
+
embedding TEXT
|
|
46
|
+
)
|
|
47
|
+
"""
|
|
48
|
+
)
|
|
49
|
+
conn.execute(
|
|
50
|
+
"CREATE INDEX IF NOT EXISTS ix_memory_type ON memory(memory_type)"
|
|
51
|
+
)
|
|
52
|
+
conn.execute(
|
|
53
|
+
"CREATE INDEX IF NOT EXISTS ix_memory_timestamp ON memory(timestamp)"
|
|
54
|
+
)
|
|
55
|
+
try:
|
|
56
|
+
conn.execute("ALTER TABLE memory ADD COLUMN run_id TEXT DEFAULT ''")
|
|
57
|
+
except sqlite3.OperationalError:
|
|
58
|
+
pass
|
|
59
|
+
try:
|
|
60
|
+
conn.execute("ALTER TABLE memory ADD COLUMN archived INTEGER DEFAULT 0")
|
|
61
|
+
except sqlite3.OperationalError:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def store(self, record: MemoryRecord) -> str:
|
|
65
|
+
"""Store a memory record. Returns record id. Redacts PII if compliance.pii_redaction enabled."""
|
|
66
|
+
content = record.content
|
|
67
|
+
try:
|
|
68
|
+
from devsper.config import get_config
|
|
69
|
+
cfg = get_config()
|
|
70
|
+
if getattr(getattr(cfg, "compliance", None), "pii_redaction", False):
|
|
71
|
+
from devsper.compliance.pii import PIIRedactor
|
|
72
|
+
redactor = PIIRedactor(
|
|
73
|
+
pii_types=getattr(cfg.compliance, "pii_types", None),
|
|
74
|
+
gdpr_mode=getattr(cfg.compliance, "gdpr_mode", False),
|
|
75
|
+
)
|
|
76
|
+
res = redactor.redact(content or "")
|
|
77
|
+
content = res.redacted_text
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
if content != record.content:
|
|
81
|
+
record = record.model_copy(update={"content": content})
|
|
82
|
+
row = record.to_store_row()
|
|
83
|
+
emb = row.get("embedding")
|
|
84
|
+
embedding_json = json.dumps(emb) if emb is not None else None
|
|
85
|
+
archived = row.get("archived", 0)
|
|
86
|
+
run_id = row.get("run_id", "") or ""
|
|
87
|
+
with self._conn() as conn:
|
|
88
|
+
conn.execute(
|
|
89
|
+
"""
|
|
90
|
+
INSERT OR REPLACE INTO memory
|
|
91
|
+
(memory_id, memory_type, content, tags, timestamp, source_task, embedding, run_id, archived)
|
|
92
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
93
|
+
""",
|
|
94
|
+
(
|
|
95
|
+
row["memory_id"],
|
|
96
|
+
row["memory_type"],
|
|
97
|
+
row["content"],
|
|
98
|
+
row["tags"],
|
|
99
|
+
row["timestamp"],
|
|
100
|
+
row["source_task"],
|
|
101
|
+
embedding_json,
|
|
102
|
+
run_id,
|
|
103
|
+
archived,
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
return row["memory_id"]
|
|
107
|
+
|
|
108
|
+
def retrieve(self, memory_id: str) -> MemoryRecord | None:
|
|
109
|
+
"""Retrieve a single record by id."""
|
|
110
|
+
with self._conn() as conn:
|
|
111
|
+
conn.row_factory = sqlite3.Row
|
|
112
|
+
cur = conn.execute(
|
|
113
|
+
"SELECT memory_id, memory_type, content, tags, timestamp, source_task, embedding, run_id, archived FROM memory WHERE memory_id = ?",
|
|
114
|
+
(memory_id,),
|
|
115
|
+
)
|
|
116
|
+
row = cur.fetchone()
|
|
117
|
+
if row is None:
|
|
118
|
+
return None
|
|
119
|
+
return _row_to_record(dict(row))
|
|
120
|
+
|
|
121
|
+
def delete(self, memory_id: str) -> bool:
|
|
122
|
+
"""Delete a record. Returns True if something was deleted."""
|
|
123
|
+
with self._conn() as conn:
|
|
124
|
+
cur = conn.execute("DELETE FROM memory WHERE memory_id = ?", (memory_id,))
|
|
125
|
+
return cur.rowcount > 0
|
|
126
|
+
|
|
127
|
+
def list_memory(
|
|
128
|
+
self,
|
|
129
|
+
memory_type: MemoryType | None = None,
|
|
130
|
+
limit: int = 100,
|
|
131
|
+
offset: int = 0,
|
|
132
|
+
tag_contains: str | None = None,
|
|
133
|
+
include_archived: bool = False,
|
|
134
|
+
run_id_filter: str | None = None,
|
|
135
|
+
) -> list[MemoryRecord]:
|
|
136
|
+
"""List records, optionally filtered by type, tag, archived, run_id, with limit/offset."""
|
|
137
|
+
with self._conn() as conn:
|
|
138
|
+
conn.row_factory = sqlite3.Row
|
|
139
|
+
conditions = []
|
|
140
|
+
params = []
|
|
141
|
+
if memory_type is not None:
|
|
142
|
+
conditions.append("memory_type = ?")
|
|
143
|
+
params.append(memory_type.value)
|
|
144
|
+
if tag_contains:
|
|
145
|
+
conditions.append("tags LIKE ?")
|
|
146
|
+
params.append(f"%{tag_contains}%")
|
|
147
|
+
if not include_archived:
|
|
148
|
+
conditions.append("COALESCE(archived, 0) = 0")
|
|
149
|
+
if run_id_filter is not None:
|
|
150
|
+
conditions.append("run_id = ?")
|
|
151
|
+
params.append(run_id_filter)
|
|
152
|
+
where = (" WHERE " + " AND ".join(conditions)) if conditions else ""
|
|
153
|
+
params.extend([limit, offset])
|
|
154
|
+
cur = conn.execute(
|
|
155
|
+
f"""
|
|
156
|
+
SELECT memory_id, memory_type, content, tags, timestamp, source_task, embedding,
|
|
157
|
+
COALESCE(run_id, '') as run_id, COALESCE(archived, 0) as archived
|
|
158
|
+
FROM memory{where} ORDER BY timestamp DESC LIMIT ? OFFSET ?
|
|
159
|
+
""",
|
|
160
|
+
params,
|
|
161
|
+
)
|
|
162
|
+
rows = cur.fetchall()
|
|
163
|
+
return [_row_to_record(dict(r)) for r in rows]
|
|
164
|
+
|
|
165
|
+
def list_all_ids(self, memory_type: MemoryType | None = None) -> list[str]:
|
|
166
|
+
"""List all memory ids (for index sync)."""
|
|
167
|
+
with self._conn() as conn:
|
|
168
|
+
if memory_type is not None:
|
|
169
|
+
cur = conn.execute(
|
|
170
|
+
"SELECT memory_id FROM memory WHERE memory_type = ?",
|
|
171
|
+
(memory_type.value,),
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
cur = conn.execute("SELECT memory_id FROM memory")
|
|
175
|
+
return [r[0] for r in cur.fetchall()]
|
|
176
|
+
|
|
177
|
+
def set_archived(self, memory_id: str, archived: bool = True) -> bool:
|
|
178
|
+
"""v1.8: Mark a record as archived (e.g. after consolidation)."""
|
|
179
|
+
with self._conn() as conn:
|
|
180
|
+
cur = conn.execute(
|
|
181
|
+
"UPDATE memory SET archived = ? WHERE memory_id = ?",
|
|
182
|
+
(1 if archived else 0, memory_id),
|
|
183
|
+
)
|
|
184
|
+
return cur.rowcount > 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _row_to_record(row: dict) -> MemoryRecord:
|
|
188
|
+
tags_str = row.get("tags") or ""
|
|
189
|
+
tags = [t.strip() for t in tags_str.split(",") if t.strip()]
|
|
190
|
+
emb_raw = row.get("embedding")
|
|
191
|
+
embedding = json.loads(emb_raw) if isinstance(emb_raw, str) and emb_raw else None
|
|
192
|
+
archived = row.get("archived")
|
|
193
|
+
if archived is None and "archived" not in row:
|
|
194
|
+
archived = 0
|
|
195
|
+
return MemoryRecord(
|
|
196
|
+
id=row["memory_id"],
|
|
197
|
+
memory_type=MemoryType(row["memory_type"]),
|
|
198
|
+
content=row["content"],
|
|
199
|
+
tags=tags,
|
|
200
|
+
timestamp=datetime.fromisoformat(row["timestamp"]),
|
|
201
|
+
source_task=row.get("source_task") or "",
|
|
202
|
+
embedding=embedding,
|
|
203
|
+
run_id=row.get("run_id") or "",
|
|
204
|
+
archived=bool(archived) if isinstance(archived, (int, bool)) else False,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def generate_memory_id() -> str:
|
|
209
|
+
"""Generate a unique memory id."""
|
|
210
|
+
return str(uuid.uuid4())
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
_default_store: MemoryStore | None = None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def get_default_store() -> MemoryStore:
|
|
217
|
+
"""Return the default process-wide memory store (for tools)."""
|
|
218
|
+
global _default_store
|
|
219
|
+
if _default_store is None:
|
|
220
|
+
_default_store = MemoryStore()
|
|
221
|
+
return _default_store
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Structured memory types for the swarm.
|
|
3
|
+
|
|
4
|
+
Each memory record has: id, timestamp, source_task, content, tags, embedding (optional).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MemoryType(str, Enum):
|
|
15
|
+
"""Kind of memory for routing and indexing."""
|
|
16
|
+
|
|
17
|
+
EPISODIC = "episodic"
|
|
18
|
+
SEMANTIC = "semantic"
|
|
19
|
+
ARTIFACT = "artifact"
|
|
20
|
+
RESEARCH = "research"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MemoryRecord(BaseModel):
|
|
24
|
+
"""Single memory entry with optional embedding."""
|
|
25
|
+
|
|
26
|
+
id: str
|
|
27
|
+
memory_type: MemoryType
|
|
28
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
29
|
+
source_task: str = ""
|
|
30
|
+
content: str
|
|
31
|
+
tags: list[str] = Field(default_factory=list)
|
|
32
|
+
embedding: list[float] | None = None
|
|
33
|
+
run_id: str = "" # v1.8: which swarm run produced this (for cross-run synthesis)
|
|
34
|
+
archived: bool = False # v1.8: consolidated into a summary record
|
|
35
|
+
|
|
36
|
+
def to_store_row(self) -> dict[str, Any]:
|
|
37
|
+
"""Serialize for storage (embedding as JSON list or null)."""
|
|
38
|
+
return {
|
|
39
|
+
"memory_id": self.id,
|
|
40
|
+
"memory_type": self.memory_type.value,
|
|
41
|
+
"content": self.content,
|
|
42
|
+
"tags": ",".join(self.tags) if self.tags else "",
|
|
43
|
+
"timestamp": self.timestamp.isoformat(),
|
|
44
|
+
"source_task": self.source_task,
|
|
45
|
+
"embedding": self.embedding,
|
|
46
|
+
"run_id": getattr(self, "run_id", "") or "",
|
|
47
|
+
"archived": 1 if getattr(self, "archived", False) else 0,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
EpisodicMemory = MemoryRecord
|
|
52
|
+
SemanticMemory = MemoryRecord
|
|
53
|
+
ArtifactMemory = MemoryRecord
|
|
54
|
+
ResearchMemory = MemoryRecord
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory namespaces: filter and tag memories by namespace (e.g. research_memory, coding_memory).
|
|
3
|
+
Implemented via tags with prefix "ns:" for backward compatibility.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from devsper.memory.memory_types import MemoryRecord
|
|
7
|
+
|
|
8
|
+
NAMESPACE_TAG_PREFIX = "ns:"
|
|
9
|
+
|
|
10
|
+
# Standard namespaces
|
|
11
|
+
RESEARCH_MEMORY = "research_memory"
|
|
12
|
+
CODING_MEMORY = "coding_memory"
|
|
13
|
+
DATASET_MEMORY = "dataset_memory"
|
|
14
|
+
|
|
15
|
+
DEFAULT_NAMESPACES = [RESEARCH_MEMORY, CODING_MEMORY, DATASET_MEMORY]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def namespace_tag(namespace: str) -> str:
|
|
19
|
+
"""Return the tag string for a namespace."""
|
|
20
|
+
return f"{NAMESPACE_TAG_PREFIX}{namespace}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def add_namespace(record: MemoryRecord, namespace: str) -> MemoryRecord:
|
|
24
|
+
"""Add namespace tag to a record (returns new record with tag)."""
|
|
25
|
+
tag = namespace_tag(namespace)
|
|
26
|
+
tags = list(record.tags) if record.tags else []
|
|
27
|
+
if tag not in tags:
|
|
28
|
+
tags.append(tag)
|
|
29
|
+
return record.model_copy(update={"tags": tags})
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def record_namespace(record: MemoryRecord) -> str | None:
|
|
33
|
+
"""Extract namespace from record tags, or None if none."""
|
|
34
|
+
if not record.tags:
|
|
35
|
+
return None
|
|
36
|
+
for t in record.tags:
|
|
37
|
+
if t.startswith(NAMESPACE_TAG_PREFIX):
|
|
38
|
+
return t[len(NAMESPACE_TAG_PREFIX) :]
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def filter_by_namespace(records: list[MemoryRecord], namespace: str) -> list[MemoryRecord]:
|
|
43
|
+
"""Return records that have the given namespace tag."""
|
|
44
|
+
tag = namespace_tag(namespace)
|
|
45
|
+
return [r for r in records if r.tags and tag in r.tags]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory scoring: relevance, recency, importance for ranking results.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from math import exp
|
|
7
|
+
|
|
8
|
+
from devsper.memory.memory_types import MemoryRecord
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def recency_score(record: MemoryRecord, now: datetime | None = None) -> float:
|
|
12
|
+
"""Score by recency: newer = higher. Exponential decay."""
|
|
13
|
+
now = now or datetime.now(timezone.utc)
|
|
14
|
+
ts = record.timestamp
|
|
15
|
+
delta = (now - ts).total_seconds()
|
|
16
|
+
# Half-life ~ 1 day: score = 0.5 at 86400 seconds
|
|
17
|
+
half_life = 86400.0
|
|
18
|
+
return exp(-delta * (0.693 / half_life))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def importance_score(record: MemoryRecord) -> float:
|
|
22
|
+
"""Heuristic importance: longer content and more tags = slightly higher."""
|
|
23
|
+
base = 1.0
|
|
24
|
+
content_len = len(record.content or "")
|
|
25
|
+
if content_len > 500:
|
|
26
|
+
base += 0.2
|
|
27
|
+
if content_len > 2000:
|
|
28
|
+
base += 0.2
|
|
29
|
+
tag_count = len(record.tags or [])
|
|
30
|
+
if tag_count > 3:
|
|
31
|
+
base += 0.1
|
|
32
|
+
return min(base, 2.0)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def combine_scores(
|
|
36
|
+
similarity: float,
|
|
37
|
+
recency: float,
|
|
38
|
+
importance: float,
|
|
39
|
+
similarity_weight: float = 0.7,
|
|
40
|
+
recency_weight: float = 0.2,
|
|
41
|
+
importance_weight: float = 0.1,
|
|
42
|
+
) -> float:
|
|
43
|
+
"""Combine normalized scores. Default: similarity dominates."""
|
|
44
|
+
return (
|
|
45
|
+
similarity_weight * similarity
|
|
46
|
+
+ recency_weight * recency
|
|
47
|
+
+ importance_weight * importance
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def score_and_sort(
|
|
52
|
+
records: list[MemoryRecord],
|
|
53
|
+
similarity_scores: list[float] | None = None,
|
|
54
|
+
now: datetime | None = None,
|
|
55
|
+
) -> list[tuple[MemoryRecord, float]]:
|
|
56
|
+
"""
|
|
57
|
+
Return (record, combined_score) sorted by score descending.
|
|
58
|
+
If similarity_scores is None, use 1.0 for all (recency + importance only).
|
|
59
|
+
"""
|
|
60
|
+
if similarity_scores is None:
|
|
61
|
+
similarity_scores = [1.0] * len(records)
|
|
62
|
+
if len(similarity_scores) != len(records):
|
|
63
|
+
similarity_scores = [1.0] * len(records)
|
|
64
|
+
now = now or datetime.now(timezone.utc)
|
|
65
|
+
scored = [
|
|
66
|
+
(
|
|
67
|
+
r,
|
|
68
|
+
combine_scores(
|
|
69
|
+
similarity_scores[i],
|
|
70
|
+
recency_score(r, now),
|
|
71
|
+
importance_score(r),
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
for i, r in enumerate(records)
|
|
75
|
+
]
|
|
76
|
+
scored.sort(key=lambda x: -x[1])
|
|
77
|
+
return scored
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory summarization: summarize a set of memory records into shorter text.
|
|
3
|
+
Optional LLM or extractive (first N chars / key sentences).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from devsper.memory.memory_types import MemoryRecord
|
|
7
|
+
|
|
8
|
+
MAX_EXTRACTIVE_CHARS = 4000
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def summarize_extractive(records: list[MemoryRecord], max_chars: int = MAX_EXTRACTIVE_CHARS) -> str:
|
|
12
|
+
"""Concatenate record contents up to max_chars (no LLM)."""
|
|
13
|
+
parts: list[str] = []
|
|
14
|
+
total = 0
|
|
15
|
+
for r in records:
|
|
16
|
+
if total >= max_chars:
|
|
17
|
+
break
|
|
18
|
+
chunk = (r.content or "")[: max_chars - total]
|
|
19
|
+
if chunk:
|
|
20
|
+
parts.append(chunk)
|
|
21
|
+
total += len(chunk)
|
|
22
|
+
return "\n\n".join(parts) if parts else ""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def summarize_with_llm(records: list[MemoryRecord], model_name: str = "gpt-4o-mini") -> str:
|
|
26
|
+
"""Use LLM to summarize records. Returns summary or fallback to extractive on failure."""
|
|
27
|
+
text = summarize_extractive(records, max_chars=8000)
|
|
28
|
+
if not text.strip():
|
|
29
|
+
return ""
|
|
30
|
+
try:
|
|
31
|
+
from devsper.utils.models import generate
|
|
32
|
+
prompt = f"""Summarize the following memory entries into a concise summary (2-4 paragraphs).
|
|
33
|
+
|
|
34
|
+
{text}
|
|
35
|
+
|
|
36
|
+
Summary:"""
|
|
37
|
+
return generate(model_name, prompt).strip()
|
|
38
|
+
except Exception:
|
|
39
|
+
return summarize_extractive(records, max_chars=MAX_EXTRACTIVE_CHARS)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def summarize(
|
|
43
|
+
records: list[MemoryRecord],
|
|
44
|
+
use_llm: bool = False,
|
|
45
|
+
model_name: str = "gpt-4o-mini",
|
|
46
|
+
) -> str:
|
|
47
|
+
"""Summarize records. use_llm=False uses extractive only."""
|
|
48
|
+
if not records:
|
|
49
|
+
return ""
|
|
50
|
+
if use_llm:
|
|
51
|
+
return summarize_with_llm(records, model_name=model_name)
|
|
52
|
+
return summarize_extractive(records)
|