clm-kernel 0.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.
- clm/__init__.py +230 -0
- clm/__main__.py +25 -0
- clm/cross_cutting/__init__.py +50 -0
- clm/cross_cutting/_test_runner.py +36 -0
- clm/cross_cutting/async_utils.py +25 -0
- clm/cross_cutting/cel_builder.py +123 -0
- clm/cross_cutting/cel_converter.py +697 -0
- clm/cross_cutting/cel_engine.py +252 -0
- clm/cross_cutting/cel_ops.py +72 -0
- clm/cross_cutting/cel_ops_db.py +157 -0
- clm/cross_cutting/cel_ops_files.py +99 -0
- clm/cross_cutting/cel_ops_state.py +71 -0
- clm/cross_cutting/cel_ops_strings.py +64 -0
- clm/cross_cutting/cel_resolver.py +63 -0
- clm/cross_cutting/cel_slices.py +56 -0
- clm/cross_cutting/cel_store.py +330 -0
- clm/cross_cutting/clm_logger.py +140 -0
- clm/cross_cutting/config/__init__.py +155 -0
- clm/cross_cutting/config/config_constants.py +392 -0
- clm/cross_cutting/config/env_parameters.py +90 -0
- clm/cross_cutting/config/logging.py +378 -0
- clm/cross_cutting/config/settings.py +504 -0
- clm/cross_cutting/context.py +5 -0
- clm/cross_cutting/domain_models.py +55 -0
- clm/cross_cutting/domain_types.py +61 -0
- clm/cross_cutting/effects.py +67 -0
- clm/cross_cutting/errors.py +153 -0
- clm/cross_cutting/governance.py +331 -0
- clm/cross_cutting/guards.py +41 -0
- clm/cross_cutting/mcard.py +321 -0
- clm/cross_cutting/native.py +185 -0
- clm/cross_cutting/observability.py +289 -0
- clm/cross_cutting/pcard.py +434 -0
- clm/cross_cutting/registry.py +71 -0
- clm/cross_cutting/resources.py +84 -0
- clm/cross_cutting/secrets.py +44 -0
- clm/cross_cutting/subprocess_utils.py +118 -0
- clm/cross_cutting/telemetry.py +164 -0
- clm/cross_cutting/testing/__init__.py +8 -0
- clm/cross_cutting/testing/comparator.py +187 -0
- clm/cross_cutting/testing/datasets.py +87 -0
- clm/cross_cutting/timeutil.py +20 -0
- clm/cross_cutting/types/new_type_check.py +0 -0
- clm/cross_cutting/utils/__init__.py +1 -0
- clm/cross_cutting/utils/url_safety.py +50 -0
- clm/cross_cutting/vcard.py +560 -0
- clm/cross_cutting/writer.py +118 -0
- clm/layer0/README.md +50 -0
- clm/layer0/__init__.py +223 -0
- clm/layer0/algebra.py +60 -0
- clm/layer0/algebra_baldwin.py +133 -0
- clm/layer0/algebra_baldwin_generators.py +36 -0
- clm/layer0/algebra_equivalence.py +262 -0
- clm/layer0/algebra_ingest.py +306 -0
- clm/layer0/algebra_manifest_io.py +156 -0
- clm/layer0/assembler.py +481 -0
- clm/layer0/cel/__init__.py +23 -0
- clm/layer0/cel_eval.py +491 -0
- clm/layer0/codec.py +106 -0
- clm/layer0/context.py +122 -0
- clm/layer0/db.py +160 -0
- clm/layer0/exceptions.py +73 -0
- clm/layer0/fibration.py +106 -0
- clm/layer0/gates.py +50 -0
- clm/layer0/hash.py +80 -0
- clm/layer0/loader.py +826 -0
- clm/layer0/math_utils.py +163 -0
- clm/layer0/mcard.py +96 -0
- clm/layer0/model/__init__.py +0 -0
- clm/layer0/model/action.py +223 -0
- clm/layer0/model/card.py +280 -0
- clm/layer0/model/card_triad.py +427 -0
- clm/layer0/model/compatibility.py +60 -0
- clm/layer0/model/dictionary.py +46 -0
- clm/layer0/model/dots.py +331 -0
- clm/layer0/model/event_producer.py +88 -0
- clm/layer0/model/g_time.py +89 -0
- clm/layer0/model/handle.py +163 -0
- clm/layer0/model/hash/__init__.py +6 -0
- clm/layer0/model/hash/algorithms/__init__.py +3 -0
- clm/layer0/model/hash/algorithms/custom_hash.py +7 -0
- clm/layer0/model/hash/algorithms/local_sha256.py +178 -0
- clm/layer0/model/hash/constants.py +53 -0
- clm/layer0/model/hash/enums.py +60 -0
- clm/layer0/model/hash/validator.py +307 -0
- clm/layer0/model/interpreter.py +275 -0
- clm/layer0/model/pagination.py +45 -0
- clm/layer0/model/pcard.py +716 -0
- clm/layer0/model/schema.py +44 -0
- clm/layer0/model/utils/__init__.py +1 -0
- clm/layer0/model/utils/content_analyzer.py +152 -0
- clm/layer0/model/validators/__init__.py +1 -0
- clm/layer0/model/validators/base_validator.py +44 -0
- clm/layer0/model/validators/binary_validator.py +66 -0
- clm/layer0/model/validators/text_validator.py +82 -0
- clm/layer0/model/validators/validation_registry.py +49 -0
- clm/layer0/model/vcard.py +823 -0
- clm/layer0/model/vcard_ext/__init__.py +39 -0
- clm/layer0/model/vcard_ext/core.py +60 -0
- clm/layer0/model/vcard_ext/network.py +23 -0
- clm/layer0/model/vcard_ext/observability.py +63 -0
- clm/layer0/model/vcard_ext/storage.py +60 -0
- clm/layer0/model/vcard_ext/vendors.py +211 -0
- clm/layer0/model/vcard_sandwich.py +119 -0
- clm/layer0/model/vcard_vocabulary.py +463 -0
- clm/layer0/model/workflow.py +274 -0
- clm/layer0/narrative.py +431 -0
- clm/layer0/ontology/collection.py +125 -0
- clm/layer0/parser.py +429 -0
- clm/layer0/relations.py +73 -0
- clm/layer0/rule_evaluator.py +251 -0
- clm/layer0/schema/__init__.py +38 -0
- clm/layer0/schema/ddl.py +97 -0
- clm/layer0/templates/abstract.yaml +99 -0
- clm/layer0/templates/balanced.yaml +253 -0
- clm/layer0/templates/concrete.yaml +172 -0
- clm/layer0/type_registry.py +223 -0
- clm/layer0/types.py +204 -0
- clm/layer0/utils.py +886 -0
- clm/layer0/verifier_core.py +262 -0
- clm/layer1/__init__.py +31 -0
- clm/layer1/action_dispatcher.py +275 -0
- clm/layer1/adapter_registry.py +233 -0
- clm/layer1/algebra_baldwin_generators.py +192 -0
- clm/layer1/arrow_compiler.py +525 -0
- clm/layer1/batch_processor.py +152 -0
- clm/layer1/cel/__init__.py +27 -0
- clm/layer1/cel/builder.py +173 -0
- clm/layer1/engine.py +759 -0
- clm/layer1/engine_async.py +89 -0
- clm/layer1/eval_callback.py +49 -0
- clm/layer1/gate_composer.py +229 -0
- clm/layer1/hoare_sandwich.py +298 -0
- clm/layer1/identity_guard.py +348 -0
- clm/layer1/matrix.py +138 -0
- clm/layer1/neural_runtime.py +221 -0
- clm/layer1/operations/__init__.py +123 -0
- clm/layer1/operations/augment.py +156 -0
- clm/layer1/operations/base.py +109 -0
- clm/layer1/operations/builtins.py +338 -0
- clm/layer1/operations/dispatch.py +144 -0
- clm/layer1/operations/exclude.py +107 -0
- clm/layer1/operations/handle.py +197 -0
- clm/layer1/operations/identity.py +139 -0
- clm/layer1/operations/invert.py +66 -0
- clm/layer1/operations/loader.py +116 -0
- clm/layer1/operations/manipulators/__init__.py +23 -0
- clm/layer1/operations/manipulators/document.py +211 -0
- clm/layer1/operations/manipulators/media.py +134 -0
- clm/layer1/operations/manipulators/structured.py +159 -0
- clm/layer1/operations/manipulators/tabular.py +334 -0
- clm/layer1/operations/port.py +65 -0
- clm/layer1/operations/services.py +529 -0
- clm/layer1/operations/split.py +117 -0
- clm/layer1/operations/substitute.py +75 -0
- clm/layer1/petri.py +1052 -0
- clm/layer1/pocketflow_orchestrator.py +408 -0
- clm/layer1/pocketflow_scheduler.py +519 -0
- clm/layer1/protocols.py +197 -0
- clm/layer1/runtime_adapter.py +718 -0
- clm/layer1/runtimes/__init__.py +71 -0
- clm/layer1/runtimes/_generated_assertions.py +125 -0
- clm/layer1/runtimes/_generated_types.py +42 -0
- clm/layer1/runtimes/base.py +265 -0
- clm/layer1/runtimes/binary.py +101 -0
- clm/layer1/runtimes/factory.py +203 -0
- clm/layer1/runtimes/javascript.py +321 -0
- clm/layer1/runtimes/javascript_runtime.js +33 -0
- clm/layer1/runtimes/js_scripts/isomorphism.mjs +22 -0
- clm/layer1/runtimes/js_scripts/loader.mjs +52 -0
- clm/layer1/runtimes/lambda_calc.py +126 -0
- clm/layer1/runtimes/python.py +288 -0
- clm/layer1/runtimes/python_runtime.py +41 -0
- clm/layer1/runtimes/script.py +114 -0
- clm/layer1/sandbox.py +334 -0
- clm/layer1/savepoint_guard.py +136 -0
- clm/layer1/sparse_net.py +122 -0
- clm/layer1/spec_tester.py +165 -0
- clm/layer1/store.py +295 -0
- clm/layer1/test_runner.py +498 -0
- clm/layer1/type_inspector.py +166 -0
- clm/layer1/verifier.py +772 -0
- clm/layer1/vm.py +91 -0
- clm/layer2/__init__.py +78 -0
- clm/layer2/card_collection.py +678 -0
- clm/layer2/classifier.py +479 -0
- clm/layer2/engine/__init__.py +1 -0
- clm/layer2/engine/abstract_sql_engine.py +320 -0
- clm/layer2/engine/base.py +193 -0
- clm/layer2/engine/duckdb_engine.py +576 -0
- clm/layer2/engine/sqlite_engine.py +518 -0
- clm/layer2/fiber/__init__.py +31 -0
- clm/layer2/fiber/context.py +89 -0
- clm/layer2/fiber/disposable.py +163 -0
- clm/layer2/fiber/fiber.py +125 -0
- clm/layer2/fiber/hoare.py +191 -0
- clm/layer2/file_io.py +376 -0
- clm/layer2/file_registrar.py +77 -0
- clm/layer2/ingest.py +202 -0
- clm/layer2/lineage.py +322 -0
- clm/layer2/mcard_fs.py +262 -0
- clm/layer2/merkle.py +11 -0
- clm/layer2/mime.py +244 -0
- clm/layer2/mime_detector.py +245 -0
- clm/layer2/private_collection.py +118 -0
- clm/layer2/shared_store.py +11 -0
- clm/layer2/storage/__init__.py +190 -0
- clm/layer2/storage/db_bitemporal.py +141 -0
- clm/layer2/storage/db_driver.py +331 -0
- clm/layer2/storage/db_hyperlinks.py +214 -0
- clm/layer2/storage/db_pool.py +139 -0
- clm/layer2/storage/db_repository.py +229 -0
- clm/layer2/storage/savepoint.py +78 -0
- clm/layer2/storage/sqlite_wal.py +74 -0
- clm/layer2/tridb.py +245 -0
- clm/layer2/vcard_receipt.py +71 -0
- clm/layer2/vfs.py +17 -0
- clm/layer3/__init__.py +141 -0
- clm/layer3/agency/__init__.py +19 -0
- clm/layer3/agency/config.py +220 -0
- clm/layer3/agency/providers/__init__.py +15 -0
- clm/layer3/agency/providers/base.py +91 -0
- clm/layer3/agency/providers/mlc_llm.py +169 -0
- clm/layer3/agency/providers/ollama.py +203 -0
- clm/layer3/agency/router.py +71 -0
- clm/layer3/agency/runtime.py +524 -0
- clm/layer3/gateway.py +353 -0
- clm/layer3/open_interpreter_adapter.py +159 -0
- clm/layer3/rag/__init__.py +70 -0
- clm/layer3/rag/cli.py +228 -0
- clm/layer3/rag/config.py +153 -0
- clm/layer3/rag/embeddings/__init__.py +16 -0
- clm/layer3/rag/embeddings/base.py +79 -0
- clm/layer3/rag/embeddings/ollama.py +211 -0
- clm/layer3/rag/embeddings/vision.py +290 -0
- clm/layer3/rag/engine.py +276 -0
- clm/layer3/rag/graph/__init__.py +22 -0
- clm/layer3/rag/graph/community.py +198 -0
- clm/layer3/rag/graph/engine.py +433 -0
- clm/layer3/rag/graph/extractor.py +255 -0
- clm/layer3/rag/graph/schema.py +115 -0
- clm/layer3/rag/graph/store.py +641 -0
- clm/layer3/rag/indexer.py +333 -0
- clm/layer3/rag/llm_providers.json +52 -0
- clm/layer3/rag/semantic_versioning.py +313 -0
- clm/layer3/rag/vector/__init__.py +28 -0
- clm/layer3/rag/vector/handle_vector_store.py +749 -0
- clm/layer3/rag/vector/schema.py +151 -0
- clm/layer3/rag/vector/store.py +632 -0
- clm/layer3/reticulum/__init__.py +55 -0
- clm/layer3/reticulum/bridge.py +74 -0
- clm/layer3/reticulum/daemon.py +88 -0
- clm/layer3/reticulum/discovery.py +164 -0
- clm/layer3/reticulum/identity.py +107 -0
- clm/layer3/reticulum/media.py +148 -0
- clm/layer3/reticulum/transport.py +133 -0
- clm/layer3/satori/__init__.py +209 -0
- clm/layer3/satori/alpha_conversion.py +168 -0
- clm/layer3/satori/beta_reduction.py +359 -0
- clm/layer3/satori/continuation.py +38 -0
- clm/layer3/satori/eta_conversion.py +205 -0
- clm/layer3/satori/free_variables.py +163 -0
- clm/layer3/satori/io_effects.py +370 -0
- clm/layer3/satori/lambda_runtime.py +726 -0
- clm/layer3/satori/lambda_term.py +230 -0
- clm/layer3/satori/sheaf_audit.py +261 -0
- clm/layer3/satori/speech_act.py +279 -0
- clm/layer4/README.md +9 -0
- clm/layer4/__init__.py +67 -0
- clm/layer4/_sovereign_executor.py +580 -0
- clm/layer4/action_commands.py +719 -0
- clm/layer4/api.py +236 -0
- clm/layer4/bootstrap/__init__.py +13 -0
- clm/layer4/bootstrap/bootstrapper.py +118 -0
- clm/layer4/bootstrap/cleanup.py +44 -0
- clm/layer4/bootstrap/clm_hooks.py +49 -0
- clm/layer4/bootstrap/genesis.py +77 -0
- clm/layer4/bootstrap/injection.py +125 -0
- clm/layer4/cli.py +549 -0
- clm/layer4/collection_manager.py +553 -0
- clm/layer4/commands/__init__.py +81 -0
- clm/layer4/commands/action_cmd.py +42 -0
- clm/layer4/commands/check_cmd.py +82 -0
- clm/layer4/commands/envelope.py +73 -0
- clm/layer4/commands/evaluate_cmd.py +224 -0
- clm/layer4/commands/mcard_cmd.py +24 -0
- clm/layer4/commands/petri_cmd.py +92 -0
- clm/layer4/commands/tridb_cmd.py +319 -0
- clm/layer4/commands/version_cmd.py +27 -0
- clm/layer4/diff_commands.py +167 -0
- clm/layer4/eoa_sponsor.py +232 -0
- clm/layer4/events.py +69 -0
- clm/layer4/flux_gateway.py +133 -0
- clm/layer4/gateway.py +91 -0
- clm/layer4/loader.py +307 -0
- clm/layer4/mcard_commands.py +166 -0
- clm/layer4/membrane.py +273 -0
- clm/layer4/navigation.py +50 -0
- clm/layer4/protocol/protocol_entry_python.py +108 -0
- clm/layer4/protocol/python_sync.py +126 -0
- clm/layer4/runner.py +150 -0
- clm/layer4/satori.py +262 -0
- clm/layer4/server/python_sync_client.py +150 -0
- clm/layer4/server/python_sync_server.py +139 -0
- clm/layer4/server/service_api.py +315 -0
- clm/layer4/server/start_servers.py +107 -0
- clm/layer4/server/stop_servers.py +24 -0
- clm/layer4/simulated_optimization_fix.py +0 -0
- clm/layer4/storage.py +463 -0
- clm/layer4/tridb_admin.py +268 -0
- clm/layer4/websocket/test_ws_client.py +104 -0
- clm/layer4/websocket/test_ws_server.py +138 -0
- clm/layer4/websocket/ws_server_local.py +160 -0
- clm/layer5/__init__.py +51 -0
- clm/layer5/async_batch_writer.py +133 -0
- clm/layer5/learning.py +276 -0
- clm/layer5/telemetry_hook.py +107 -0
- clm/layer5/type_lattice.py +238 -0
- clm/py.typed +0 -0
- clm/schemas/__init__.py +652 -0
- clm/schemas/abstract_face_schema.yaml +77 -0
- clm/schemas/cel_schema.yaml +416 -0
- clm/schemas/lifecycle/__init__.py +45 -0
- clm/schemas/lifecycle/models.py +123 -0
- clm/schemas/lifecycle/service.py +364 -0
- clm/schemas/mcard_schema.sql +192 -0
- clm/schemas/mcard_vector_schema.sql +283 -0
- clm/schemas/mlp-context-v1.json +91 -0
- clm_kernel-0.0.0.dist-info/METADATA +121 -0
- clm_kernel-0.0.0.dist-info/RECORD +333 -0
- clm_kernel-0.0.0.dist-info/WHEEL +4 -0
- clm_kernel-0.0.0.dist-info/entry_points.txt +3 -0
- clm_kernel-0.0.0.dist-info/licenses/LICENSE +21 -0
clm/layer3/rag/cli.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MCard RAG CLI
|
|
4
|
+
|
|
5
|
+
Command-line interface for RAG operations.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
mcard-rag index # Index all MCards
|
|
9
|
+
mcard-rag search "query" # Semantic search
|
|
10
|
+
mcard-rag query "question" # RAG query with LLM
|
|
11
|
+
mcard-rag status # Show indexing status
|
|
12
|
+
mcard-rag clear # Clear vector index
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import logging
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def cmd_status(args):
|
|
24
|
+
"""Show RAG system status."""
|
|
25
|
+
from clm.layer3.rag import get_indexer
|
|
26
|
+
from clm.layer3.rag.embeddings import OllamaEmbeddingProvider
|
|
27
|
+
|
|
28
|
+
logger.info("=" * 60)
|
|
29
|
+
logger.info("MCard RAG System Status")
|
|
30
|
+
logger.info("=" * 60)
|
|
31
|
+
|
|
32
|
+
# Check embedding provider
|
|
33
|
+
provider = OllamaEmbeddingProvider()
|
|
34
|
+
available = provider.validate_connection()
|
|
35
|
+
logger.info(f"\n📦 Embedding Model: {provider.model_name}")
|
|
36
|
+
logger.info(f" Status: {'✅ Available' if available else '❌ Not available'}")
|
|
37
|
+
logger.info(f" Dimensions: {provider.dimensions}")
|
|
38
|
+
|
|
39
|
+
# Get indexer stats
|
|
40
|
+
indexer = get_indexer()
|
|
41
|
+
stats = indexer.get_stats()
|
|
42
|
+
|
|
43
|
+
logger.info("\n📊 Vector Index:")
|
|
44
|
+
logger.info(f" Database: {stats['vector_db_path']}")
|
|
45
|
+
logger.info(f" Indexed MCards: {stats['unique_mcards']}")
|
|
46
|
+
logger.info(f" Total Vectors: {stats['vector_count']}")
|
|
47
|
+
logger.info(
|
|
48
|
+
f" sqlite-vec: {'✅' if stats['has_vec_extension'] else '❌ (fallback mode)'}"
|
|
49
|
+
)
|
|
50
|
+
logger.info(f" Hybrid Search: {'✅' if stats['hybrid_search_enabled'] else '❌'}")
|
|
51
|
+
|
|
52
|
+
# Show MCard collection count
|
|
53
|
+
from clm.layer2.card_collection import get_default_collection
|
|
54
|
+
default_collection = get_default_collection()
|
|
55
|
+
|
|
56
|
+
total_mcards = default_collection.count()
|
|
57
|
+
indexed = stats["unique_mcards"]
|
|
58
|
+
pending = total_mcards - indexed
|
|
59
|
+
|
|
60
|
+
logger.info("\n📁 Collection:")
|
|
61
|
+
logger.info(f" Total MCards: {total_mcards}")
|
|
62
|
+
logger.info(f" Indexed: {indexed}")
|
|
63
|
+
logger.info(f" Pending: {pending}")
|
|
64
|
+
|
|
65
|
+
if pending > 0:
|
|
66
|
+
logger.info(f"\n💡 Run 'mcard-rag index' to index {pending} pending cards")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def cmd_index(args):
|
|
70
|
+
"""Index all MCards."""
|
|
71
|
+
from clm.layer3.rag import get_indexer
|
|
72
|
+
|
|
73
|
+
logger.info("=" * 60)
|
|
74
|
+
logger.info("Indexing MCards")
|
|
75
|
+
logger.info("=" * 60)
|
|
76
|
+
|
|
77
|
+
indexer = get_indexer()
|
|
78
|
+
|
|
79
|
+
def progress(current, total):
|
|
80
|
+
pct = (current / total) * 100 if total > 0 else 0
|
|
81
|
+
print(f"\r Progress: {current}/{total} ({pct:.1f}%)", end="", flush=True)
|
|
82
|
+
|
|
83
|
+
logger.info("\n📊 Starting index...")
|
|
84
|
+
stats = indexer.index_all(force=args.force, progress_callback=progress)
|
|
85
|
+
|
|
86
|
+
logger.info("\n\n✅ Indexing Complete:")
|
|
87
|
+
logger.info(f" Indexed: {stats['indexed']}")
|
|
88
|
+
logger.info(f" Skipped: {stats['skipped']}")
|
|
89
|
+
logger.info(f" Failed: {stats['failed']}")
|
|
90
|
+
logger.info(f" Total: {stats['total']}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def cmd_search(args):
|
|
94
|
+
"""Semantic search."""
|
|
95
|
+
from clm.layer3.rag import semantic_search
|
|
96
|
+
|
|
97
|
+
logger.info("=" * 60)
|
|
98
|
+
logger.info(f"Semantic Search: {args.query}")
|
|
99
|
+
logger.info("=" * 60)
|
|
100
|
+
|
|
101
|
+
results = semantic_search(args.query, k=args.k)
|
|
102
|
+
|
|
103
|
+
if not results:
|
|
104
|
+
logger.info("\n❌ No results found")
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
logger.info(f"\n📋 Results ({len(results)} found):\n")
|
|
108
|
+
|
|
109
|
+
for i, result in enumerate(results):
|
|
110
|
+
logger.info(f"[{i + 1}] Score: {result.score:.4f}")
|
|
111
|
+
logger.info(f" Hash: {result.hash}")
|
|
112
|
+
if result.chunk_text:
|
|
113
|
+
preview = result.chunk_text[:200].replace("\n", " ")
|
|
114
|
+
logger.info(f" Preview: {preview}...")
|
|
115
|
+
logger.info("")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def cmd_query(args):
|
|
119
|
+
"""RAG query with LLM."""
|
|
120
|
+
from clm.layer3.rag import MCardRAGEngine, get_indexer
|
|
121
|
+
|
|
122
|
+
logger.info("=" * 60)
|
|
123
|
+
logger.info(f"RAG Query: {args.question}")
|
|
124
|
+
logger.info("=" * 60)
|
|
125
|
+
|
|
126
|
+
# Use persistent indexer's vector store
|
|
127
|
+
indexer = get_indexer()
|
|
128
|
+
|
|
129
|
+
rag = MCardRAGEngine(vector_db_path=indexer.vector_db_path)
|
|
130
|
+
rag.vector_store = indexer.vector_store
|
|
131
|
+
|
|
132
|
+
logger.info("\n🔍 Searching for context...")
|
|
133
|
+
response = rag.query(args.question, k=args.k, model=args.model)
|
|
134
|
+
|
|
135
|
+
logger.info(f"\n💬 Answer:\n{response.answer}")
|
|
136
|
+
logger.info(f"\n📊 Confidence: {response.confidence:.2%}")
|
|
137
|
+
logger.info(f"📚 Sources: {[h[:8] for h in response.sources]}")
|
|
138
|
+
|
|
139
|
+
if args.verbose:
|
|
140
|
+
logger.info("\n📝 Source Chunks:")
|
|
141
|
+
for i, chunk in enumerate(response.source_chunks):
|
|
142
|
+
if chunk:
|
|
143
|
+
logger.info(f" [{i + 1}] {chunk[:150]}...")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def cmd_clear(args):
|
|
147
|
+
"""Clear vector index."""
|
|
148
|
+
from clm.layer3.rag import get_indexer
|
|
149
|
+
|
|
150
|
+
if not args.yes:
|
|
151
|
+
confirm = input("Are you sure you want to clear the vector index? [y/N]: ")
|
|
152
|
+
if confirm.lower() != "y":
|
|
153
|
+
logger.info("Cancelled")
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
indexer = get_indexer()
|
|
157
|
+
indexer.clear()
|
|
158
|
+
logger.info("✅ Vector index cleared")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def main():
|
|
162
|
+
parser = argparse.ArgumentParser(
|
|
163
|
+
description="MCard RAG CLI",
|
|
164
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
168
|
+
|
|
169
|
+
# Status command
|
|
170
|
+
status_parser = subparsers.add_parser("status", help="Show RAG system status")
|
|
171
|
+
status_parser.set_defaults(func=cmd_status)
|
|
172
|
+
|
|
173
|
+
# Index command
|
|
174
|
+
index_parser = subparsers.add_parser("index", help="Index all MCards")
|
|
175
|
+
index_parser.add_argument(
|
|
176
|
+
"--force", "-f", action="store_true", help="Re-index existing cards"
|
|
177
|
+
)
|
|
178
|
+
index_parser.set_defaults(func=cmd_index)
|
|
179
|
+
|
|
180
|
+
# Search command
|
|
181
|
+
search_parser = subparsers.add_parser("search", help="Semantic search")
|
|
182
|
+
search_parser.add_argument("query", help="Search query")
|
|
183
|
+
search_parser.add_argument(
|
|
184
|
+
"-k", type=int, default=5, help="Number of results (default: 5)"
|
|
185
|
+
)
|
|
186
|
+
search_parser.set_defaults(func=cmd_search)
|
|
187
|
+
|
|
188
|
+
# Query command
|
|
189
|
+
query_parser = subparsers.add_parser("query", help="RAG query with LLM")
|
|
190
|
+
query_parser.add_argument("question", help="Question to answer")
|
|
191
|
+
query_parser.add_argument(
|
|
192
|
+
"-k", type=int, default=5, help="Number of context chunks (default: 5)"
|
|
193
|
+
)
|
|
194
|
+
query_parser.add_argument(
|
|
195
|
+
"--model",
|
|
196
|
+
"-m",
|
|
197
|
+
default="qwen3:0.6b",
|
|
198
|
+
help="LLM model (default: qwen3:0.6b)",
|
|
199
|
+
)
|
|
200
|
+
query_parser.add_argument(
|
|
201
|
+
"--verbose", "-v", action="store_true", help="Show source chunks"
|
|
202
|
+
)
|
|
203
|
+
query_parser.set_defaults(func=cmd_query)
|
|
204
|
+
|
|
205
|
+
# Clear command
|
|
206
|
+
clear_parser = subparsers.add_parser("clear", help="Clear vector index")
|
|
207
|
+
clear_parser.add_argument(
|
|
208
|
+
"--yes", "-y", action="store_true", help="Skip confirmation"
|
|
209
|
+
)
|
|
210
|
+
clear_parser.set_defaults(func=cmd_clear)
|
|
211
|
+
|
|
212
|
+
args = parser.parse_args()
|
|
213
|
+
|
|
214
|
+
if args.command is None:
|
|
215
|
+
parser.print_help()
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
args.func(args)
|
|
220
|
+
except Exception as e:
|
|
221
|
+
logger.error(f"Error: {e}", exc_info=True)
|
|
222
|
+
if "--debug" in sys.argv:
|
|
223
|
+
raise
|
|
224
|
+
sys.exit(1)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
if __name__ == "__main__":
|
|
228
|
+
main()
|
clm/layer3/rag/config.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
# Load Centralized LLM Provider Configuration
|
|
6
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _load_llm_config():
|
|
10
|
+
"""Load models from centralized JSON file."""
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
local_path = Path(__file__).resolve().parent / "llm_providers.json"
|
|
14
|
+
if local_path.exists():
|
|
15
|
+
try:
|
|
16
|
+
with open(local_path, encoding="utf-8") as f:
|
|
17
|
+
return json.load(f)
|
|
18
|
+
except Exception as e:
|
|
19
|
+
print(f"Warning: Failed to load {local_path}: {e}")
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from clm.cross_cutting.config.config_constants import _find_project_root
|
|
23
|
+
root_dir = _find_project_root()
|
|
24
|
+
config_path = root_dir / "llm_providers.json"
|
|
25
|
+
if config_path.exists():
|
|
26
|
+
with open(config_path, encoding="utf-8") as f:
|
|
27
|
+
return json.load(f)
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
curr = Path(__file__).resolve()
|
|
32
|
+
for parent in list(curr.parents):
|
|
33
|
+
cand = parent / "llm_providers.json"
|
|
34
|
+
if cand.exists():
|
|
35
|
+
try:
|
|
36
|
+
with open(cand, encoding="utf-8") as f:
|
|
37
|
+
return json.load(f)
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
# Fallback if file missing or error
|
|
42
|
+
return {
|
|
43
|
+
"embedding_models": {
|
|
44
|
+
"nomic-embed-text": {
|
|
45
|
+
"dimensions": 768,
|
|
46
|
+
"provider": "ollama",
|
|
47
|
+
"max_tokens": 8192,
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"vision_models": {"moondream": {"description": "Fallback Moondream"}},
|
|
51
|
+
"default_embedding_model": "nomic-embed-text",
|
|
52
|
+
"default_vision_model": "moondream",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_RAW_CONFIG = _load_llm_config()
|
|
57
|
+
|
|
58
|
+
EMBEDDING_MODELS = _RAW_CONFIG.get("embedding_models", {})
|
|
59
|
+
DEFAULT_EMBEDDING_MODEL = _RAW_CONFIG.get("default_embedding_model", "nomic-embed-text")
|
|
60
|
+
|
|
61
|
+
VISION_MODELS = _RAW_CONFIG.get("vision_models", {})
|
|
62
|
+
DEFAULT_VISION_MODEL = _RAW_CONFIG.get("default_vision_model", "moondream")
|
|
63
|
+
|
|
64
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
65
|
+
# Vector Search Configuration
|
|
66
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
# Distance metrics supported by sqlite-vec
|
|
69
|
+
DISTANCE_METRICS = ("L2", "cosine", "L1")
|
|
70
|
+
DEFAULT_DISTANCE_METRIC = "cosine"
|
|
71
|
+
|
|
72
|
+
# Default search parameters
|
|
73
|
+
DEFAULT_TOP_K = 5
|
|
74
|
+
MAX_TOP_K = 100
|
|
75
|
+
|
|
76
|
+
# Chunking configuration
|
|
77
|
+
DEFAULT_CHUNK_SIZE = 1000 # characters
|
|
78
|
+
DEFAULT_CHUNK_OVERLAP = 200
|
|
79
|
+
|
|
80
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
# RAG Configuration Dataclass
|
|
82
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class RAGConfig:
|
|
87
|
+
"""
|
|
88
|
+
Configuration for MCard RAG system.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
embedding_model: Name of the embedding model to use
|
|
92
|
+
embedding_provider: Provider for embeddings (ollama, openai, etc.)
|
|
93
|
+
ollama_base_url: Base URL for Ollama API
|
|
94
|
+
vector_db_path: Path to vector database (None = use main MCard db)
|
|
95
|
+
distance_metric: Distance metric for similarity search
|
|
96
|
+
top_k: Default number of results to return
|
|
97
|
+
chunk_size: Maximum characters per chunk
|
|
98
|
+
chunk_overlap: Overlap between chunks
|
|
99
|
+
enable_hybrid_search: Enable FTS + vector hybrid search
|
|
100
|
+
rerank_results: Re-rank results using cross-encoder
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
# Embedding configuration
|
|
104
|
+
embedding_model: str = DEFAULT_EMBEDDING_MODEL
|
|
105
|
+
embedding_provider: str = "ollama"
|
|
106
|
+
ollama_base_url: str = "http://localhost:11434"
|
|
107
|
+
|
|
108
|
+
# Vector storage
|
|
109
|
+
vector_db_path: str | None = None # None = same as MCard db
|
|
110
|
+
distance_metric: str = DEFAULT_DISTANCE_METRIC
|
|
111
|
+
|
|
112
|
+
# Search parameters
|
|
113
|
+
top_k: int = DEFAULT_TOP_K
|
|
114
|
+
similarity_threshold: float = 0.0 # Minimum similarity (0-1)
|
|
115
|
+
|
|
116
|
+
# Chunking
|
|
117
|
+
chunk_size: int = DEFAULT_CHUNK_SIZE
|
|
118
|
+
chunk_overlap: int = DEFAULT_CHUNK_OVERLAP
|
|
119
|
+
|
|
120
|
+
# Advanced features
|
|
121
|
+
enable_hybrid_search: bool = True
|
|
122
|
+
rerank_results: bool = False
|
|
123
|
+
|
|
124
|
+
def __post_init__(self):
|
|
125
|
+
"""Validate configuration after initialization."""
|
|
126
|
+
if self.embedding_model not in EMBEDDING_MODELS:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"Unknown embedding model: {self.embedding_model}. "
|
|
129
|
+
f"Available: {list(EMBEDDING_MODELS.keys())}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if self.distance_metric not in DISTANCE_METRICS:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"Unknown distance metric: {self.distance_metric}. "
|
|
135
|
+
f"Available: {DISTANCE_METRICS}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if self.top_k < 1 or self.top_k > MAX_TOP_K:
|
|
139
|
+
raise ValueError(f"top_k must be between 1 and {MAX_TOP_K}")
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def dimensions(self) -> int:
|
|
143
|
+
"""Get embedding dimensions for the configured model."""
|
|
144
|
+
return EMBEDDING_MODELS[self.embedding_model]["dimensions"]
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def max_tokens(self) -> int:
|
|
148
|
+
"""Get max tokens for the configured model."""
|
|
149
|
+
return EMBEDDING_MODELS[self.embedding_model]["max_tokens"]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# Default configuration
|
|
153
|
+
DEFAULT_RAG_CONFIG = RAGConfig()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Embedding Providers Package
|
|
3
|
+
|
|
4
|
+
Provides embedding generation capabilities for RAG.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .base import EmbeddingProvider
|
|
8
|
+
from .ollama import OllamaEmbeddingProvider
|
|
9
|
+
from .vision import VISION_MODELS, VisionEmbeddingProvider
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"EmbeddingProvider",
|
|
13
|
+
"OllamaEmbeddingProvider",
|
|
14
|
+
"VisionEmbeddingProvider",
|
|
15
|
+
"VISION_MODELS",
|
|
16
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Embedding Provider Base Class
|
|
3
|
+
|
|
4
|
+
Abstract interface for embedding providers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class EmbeddingResult:
|
|
13
|
+
"""Result from an embedding operation."""
|
|
14
|
+
|
|
15
|
+
vector: list[float]
|
|
16
|
+
model: str
|
|
17
|
+
dimensions: int
|
|
18
|
+
token_count: int | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EmbeddingProvider(ABC):
|
|
22
|
+
"""
|
|
23
|
+
Abstract base class for embedding providers.
|
|
24
|
+
|
|
25
|
+
Implementations should handle:
|
|
26
|
+
- Connection to the embedding service
|
|
27
|
+
- Text preprocessing (truncation, etc.)
|
|
28
|
+
- Batch processing for efficiency
|
|
29
|
+
- Error handling and retries
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
provider_name: str = "base"
|
|
33
|
+
model_name: str
|
|
34
|
+
dimensions: int
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def embed(self, text: str) -> list[float]:
|
|
38
|
+
"""
|
|
39
|
+
Generate embedding vector for a single text.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
text: Input text to embed
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
List of floats representing the embedding vector
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def embed_batch(self, texts: list[str]) -> list[list[float]]:
|
|
51
|
+
"""
|
|
52
|
+
Generate embeddings for multiple texts.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
texts: List of input texts
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List of embedding vectors
|
|
59
|
+
"""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def validate_connection(self) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Check if the embedding service is available.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
True if connection is valid
|
|
69
|
+
"""
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
def get_info(self) -> dict:
|
|
73
|
+
"""Get provider information."""
|
|
74
|
+
return {
|
|
75
|
+
"provider": self.provider_name,
|
|
76
|
+
"model": self.model_name,
|
|
77
|
+
"dimensions": self.dimensions,
|
|
78
|
+
"available": self.validate_connection(),
|
|
79
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ollama Embedding Provider
|
|
3
|
+
|
|
4
|
+
Generates embeddings using Ollama's local embedding models.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import urllib.error
|
|
10
|
+
import urllib.request
|
|
11
|
+
|
|
12
|
+
from clm.cross_cutting.config.config_constants import (
|
|
13
|
+
RAG_OLLAMA_TIMEOUT_SECS,
|
|
14
|
+
RAG_OLLAMA_VALIDATE_TIMEOUT_SECS,
|
|
15
|
+
)
|
|
16
|
+
from clm.cross_cutting.utils.url_safety import safe_urlopen
|
|
17
|
+
|
|
18
|
+
from ..config import DEFAULT_EMBEDDING_MODEL, EMBEDDING_MODELS
|
|
19
|
+
from .base import EmbeddingProvider
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OllamaEmbeddingProvider(EmbeddingProvider):
|
|
25
|
+
"""
|
|
26
|
+
Embedding provider using Ollama's embedding API.
|
|
27
|
+
|
|
28
|
+
Supports models like:
|
|
29
|
+
- nomic-embed-text (768 dimensions)
|
|
30
|
+
- bge-m3 (1024 dimensions)
|
|
31
|
+
- mxbai-embed-large (1024 dimensions)
|
|
32
|
+
- all-minilm (384 dimensions)
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
provider = OllamaEmbeddingProvider('nomic-embed-text')
|
|
36
|
+
vector = provider.embed("Hello world")
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
provider_name = "ollama"
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
model: str = DEFAULT_EMBEDDING_MODEL,
|
|
44
|
+
base_url: str = "http://localhost:11434",
|
|
45
|
+
timeout: int = RAG_OLLAMA_TIMEOUT_SECS,
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Initialize Ollama embedding provider.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
model: Ollama embedding model name
|
|
52
|
+
base_url: Ollama API base URL
|
|
53
|
+
timeout: Request timeout in seconds
|
|
54
|
+
"""
|
|
55
|
+
self.model_name = model
|
|
56
|
+
self.base_url = base_url.rstrip("/")
|
|
57
|
+
self.timeout = timeout
|
|
58
|
+
|
|
59
|
+
# Get dimensions from config or default
|
|
60
|
+
if model in EMBEDDING_MODELS:
|
|
61
|
+
self.dimensions = EMBEDDING_MODELS[model]["dimensions"]
|
|
62
|
+
else:
|
|
63
|
+
# Unknown model, will determine dimensions on first embed
|
|
64
|
+
self.dimensions = 0
|
|
65
|
+
logger.warning(
|
|
66
|
+
f"Unknown model '{model}', dimensions will be determined on first embed"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def _make_request(self, endpoint: str, data: dict) -> dict:
|
|
70
|
+
"""Make HTTP request to Ollama API."""
|
|
71
|
+
url = f"{self.base_url}{endpoint}"
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
request = urllib.request.Request(
|
|
75
|
+
url,
|
|
76
|
+
data=json.dumps(data).encode("utf-8"),
|
|
77
|
+
headers={"Content-Type": "application/json"},
|
|
78
|
+
method="POST",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
with safe_urlopen(request, timeout=self.timeout) as response:
|
|
82
|
+
return json.loads(response.read().decode("utf-8"))
|
|
83
|
+
|
|
84
|
+
except urllib.error.HTTPError as e:
|
|
85
|
+
error_body = e.read().decode("utf-8") if e.fp else str(e)
|
|
86
|
+
raise RuntimeError(f"Ollama HTTP error {e.code}: {error_body}") from e
|
|
87
|
+
except urllib.error.URLError as e:
|
|
88
|
+
raise RuntimeError(f"Ollama connection error: {e.reason}") from e
|
|
89
|
+
except Exception as e:
|
|
90
|
+
raise RuntimeError(f"Ollama request failed: {e}") from e
|
|
91
|
+
|
|
92
|
+
def embed(self, text: str) -> list[float]:
|
|
93
|
+
"""
|
|
94
|
+
Generate embedding for a single text.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
text: Input text to embed
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Embedding vector as list of floats
|
|
101
|
+
"""
|
|
102
|
+
if not text or not text.strip():
|
|
103
|
+
raise ValueError("Cannot embed empty text")
|
|
104
|
+
|
|
105
|
+
response = self._make_request(
|
|
106
|
+
"/api/embed",
|
|
107
|
+
{
|
|
108
|
+
"model": self.model_name,
|
|
109
|
+
"input": text,
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if "embeddings" in response and response["embeddings"]:
|
|
114
|
+
embedding = response["embeddings"][0]
|
|
115
|
+
|
|
116
|
+
# Update dimensions if unknown
|
|
117
|
+
if self.dimensions == 0:
|
|
118
|
+
self.dimensions = len(embedding)
|
|
119
|
+
logger.info(
|
|
120
|
+
f"Determined {self.model_name} dimensions: {self.dimensions}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return embedding
|
|
124
|
+
else:
|
|
125
|
+
raise RuntimeError(f"Unexpected Ollama response: {response}")
|
|
126
|
+
|
|
127
|
+
def embed_batch(self, texts: list[str]) -> list[list[float]]:
|
|
128
|
+
"""
|
|
129
|
+
Generate embeddings for multiple texts.
|
|
130
|
+
|
|
131
|
+
Ollama supports batch embedding with the 'input' parameter as a list.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
texts: List of input texts
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
List of embedding vectors
|
|
138
|
+
"""
|
|
139
|
+
if not texts:
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
# Filter empty texts
|
|
143
|
+
valid_texts = [t for t in texts if t and t.strip()]
|
|
144
|
+
if not valid_texts:
|
|
145
|
+
raise ValueError("All texts are empty")
|
|
146
|
+
|
|
147
|
+
response = self._make_request(
|
|
148
|
+
"/api/embed",
|
|
149
|
+
{
|
|
150
|
+
"model": self.model_name,
|
|
151
|
+
"input": valid_texts,
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if "embeddings" in response:
|
|
156
|
+
embeddings = response["embeddings"]
|
|
157
|
+
|
|
158
|
+
# Update dimensions if unknown
|
|
159
|
+
if self.dimensions == 0 and embeddings:
|
|
160
|
+
self.dimensions = len(embeddings[0])
|
|
161
|
+
|
|
162
|
+
return embeddings
|
|
163
|
+
else:
|
|
164
|
+
raise RuntimeError(f"Unexpected Ollama batch response: {response}")
|
|
165
|
+
|
|
166
|
+
def validate_connection(self) -> bool:
|
|
167
|
+
"""Check if Ollama is running and model is available."""
|
|
168
|
+
try:
|
|
169
|
+
# Check if server is running
|
|
170
|
+
request = urllib.request.Request(f"{self.base_url}/api/tags", method="GET")
|
|
171
|
+
|
|
172
|
+
with safe_urlopen(
|
|
173
|
+
request, timeout=RAG_OLLAMA_VALIDATE_TIMEOUT_SECS
|
|
174
|
+
) as response:
|
|
175
|
+
data = json.loads(response.read().decode("utf-8"))
|
|
176
|
+
|
|
177
|
+
# Check if model is available
|
|
178
|
+
if "models" in data:
|
|
179
|
+
available_models = [m.get("name", "") for m in data["models"]]
|
|
180
|
+
|
|
181
|
+
# Check exact match or partial match
|
|
182
|
+
for model in available_models:
|
|
183
|
+
if self.model_name in model or model in self.model_name:
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
logger.warning(
|
|
187
|
+
f"Model '{self.model_name}' not found. "
|
|
188
|
+
f"Available: {available_models[:5]}..."
|
|
189
|
+
)
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logger.debug(f"Ollama validation failed: {e}")
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
def pull_model(self) -> bool:
|
|
199
|
+
"""Pull the embedding model from Ollama."""
|
|
200
|
+
try:
|
|
201
|
+
self._make_request(
|
|
202
|
+
"/api/pull",
|
|
203
|
+
{
|
|
204
|
+
"name": self.model_name,
|
|
205
|
+
"stream": False,
|
|
206
|
+
},
|
|
207
|
+
)
|
|
208
|
+
return True
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.error(f"Failed to pull model {self.model_name}: {e}", exc_info=True)
|
|
211
|
+
return False
|