mantisdk 0.1.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.
Potentially problematic release.
This version of mantisdk might be problematic. Click here for more details.
- mantisdk/__init__.py +22 -0
- mantisdk/adapter/__init__.py +15 -0
- mantisdk/adapter/base.py +94 -0
- mantisdk/adapter/messages.py +270 -0
- mantisdk/adapter/triplet.py +1028 -0
- mantisdk/algorithm/__init__.py +39 -0
- mantisdk/algorithm/apo/__init__.py +5 -0
- mantisdk/algorithm/apo/apo.py +889 -0
- mantisdk/algorithm/apo/prompts/apply_edit_variant01.poml +22 -0
- mantisdk/algorithm/apo/prompts/apply_edit_variant02.poml +18 -0
- mantisdk/algorithm/apo/prompts/text_gradient_variant01.poml +18 -0
- mantisdk/algorithm/apo/prompts/text_gradient_variant02.poml +16 -0
- mantisdk/algorithm/apo/prompts/text_gradient_variant03.poml +107 -0
- mantisdk/algorithm/base.py +162 -0
- mantisdk/algorithm/decorator.py +264 -0
- mantisdk/algorithm/fast.py +250 -0
- mantisdk/algorithm/gepa/__init__.py +59 -0
- mantisdk/algorithm/gepa/adapter.py +459 -0
- mantisdk/algorithm/gepa/gepa.py +364 -0
- mantisdk/algorithm/gepa/lib/__init__.py +18 -0
- mantisdk/algorithm/gepa/lib/adapters/README.md +12 -0
- mantisdk/algorithm/gepa/lib/adapters/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/adapters/anymaths_adapter/README.md +341 -0
- mantisdk/algorithm/gepa/lib/adapters/anymaths_adapter/__init__.py +1 -0
- mantisdk/algorithm/gepa/lib/adapters/anymaths_adapter/anymaths_adapter.py +174 -0
- mantisdk/algorithm/gepa/lib/adapters/anymaths_adapter/requirements.txt +1 -0
- mantisdk/algorithm/gepa/lib/adapters/default_adapter/README.md +0 -0
- mantisdk/algorithm/gepa/lib/adapters/default_adapter/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/adapters/default_adapter/default_adapter.py +209 -0
- mantisdk/algorithm/gepa/lib/adapters/dspy_adapter/README.md +7 -0
- mantisdk/algorithm/gepa/lib/adapters/dspy_adapter/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/adapters/dspy_adapter/dspy_adapter.py +307 -0
- mantisdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/README.md +99 -0
- mantisdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/dspy_program_proposal_signature.py +137 -0
- mantisdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/full_program_adapter.py +266 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/GEPA_RAG.md +621 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/__init__.py +56 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/evaluation_metrics.py +226 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/generic_rag_adapter.py +496 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/rag_pipeline.py +238 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_store_interface.py +212 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/__init__.py +2 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/chroma_store.py +196 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/lancedb_store.py +422 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/milvus_store.py +409 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/qdrant_store.py +368 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/weaviate_store.py +418 -0
- mantisdk/algorithm/gepa/lib/adapters/mcp_adapter/README.md +552 -0
- mantisdk/algorithm/gepa/lib/adapters/mcp_adapter/__init__.py +37 -0
- mantisdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_adapter.py +705 -0
- mantisdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_client.py +364 -0
- mantisdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/README.md +9 -0
- mantisdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/terminal_bench_adapter.py +217 -0
- mantisdk/algorithm/gepa/lib/api.py +375 -0
- mantisdk/algorithm/gepa/lib/core/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/core/adapter.py +180 -0
- mantisdk/algorithm/gepa/lib/core/data_loader.py +74 -0
- mantisdk/algorithm/gepa/lib/core/engine.py +356 -0
- mantisdk/algorithm/gepa/lib/core/result.py +233 -0
- mantisdk/algorithm/gepa/lib/core/state.py +636 -0
- mantisdk/algorithm/gepa/lib/examples/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/examples/aime.py +24 -0
- mantisdk/algorithm/gepa/lib/examples/anymaths-bench/eval_default.py +111 -0
- mantisdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/instruction_prompt.txt +9 -0
- mantisdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/optimal_prompt.txt +24 -0
- mantisdk/algorithm/gepa/lib/examples/anymaths-bench/train_anymaths.py +177 -0
- mantisdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/arc_agi.ipynb +25705 -0
- mantisdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/example.ipynb +348 -0
- mantisdk/algorithm/gepa/lib/examples/mcp_adapter/__init__.py +4 -0
- mantisdk/algorithm/gepa/lib/examples/mcp_adapter/mcp_optimization_example.py +455 -0
- mantisdk/algorithm/gepa/lib/examples/rag_adapter/RAG_GUIDE.md +613 -0
- mantisdk/algorithm/gepa/lib/examples/rag_adapter/__init__.py +9 -0
- mantisdk/algorithm/gepa/lib/examples/rag_adapter/rag_optimization.py +824 -0
- mantisdk/algorithm/gepa/lib/examples/rag_adapter/requirements-rag.txt +29 -0
- mantisdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/instruction_prompt.txt +16 -0
- mantisdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/terminus.txt +9 -0
- mantisdk/algorithm/gepa/lib/examples/terminal-bench/train_terminus.py +161 -0
- mantisdk/algorithm/gepa/lib/gepa_utils.py +117 -0
- mantisdk/algorithm/gepa/lib/logging/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/logging/experiment_tracker.py +187 -0
- mantisdk/algorithm/gepa/lib/logging/logger.py +75 -0
- mantisdk/algorithm/gepa/lib/logging/utils.py +103 -0
- mantisdk/algorithm/gepa/lib/proposer/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/proposer/base.py +31 -0
- mantisdk/algorithm/gepa/lib/proposer/merge.py +357 -0
- mantisdk/algorithm/gepa/lib/proposer/reflective_mutation/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/proposer/reflective_mutation/base.py +49 -0
- mantisdk/algorithm/gepa/lib/proposer/reflective_mutation/reflective_mutation.py +176 -0
- mantisdk/algorithm/gepa/lib/py.typed +0 -0
- mantisdk/algorithm/gepa/lib/strategies/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/strategies/batch_sampler.py +77 -0
- mantisdk/algorithm/gepa/lib/strategies/candidate_selector.py +50 -0
- mantisdk/algorithm/gepa/lib/strategies/component_selector.py +36 -0
- mantisdk/algorithm/gepa/lib/strategies/eval_policy.py +64 -0
- mantisdk/algorithm/gepa/lib/strategies/instruction_proposal.py +127 -0
- mantisdk/algorithm/gepa/lib/utils/__init__.py +10 -0
- mantisdk/algorithm/gepa/lib/utils/stop_condition.py +196 -0
- mantisdk/algorithm/gepa/tracing.py +105 -0
- mantisdk/algorithm/utils.py +177 -0
- mantisdk/algorithm/verl/__init__.py +5 -0
- mantisdk/algorithm/verl/interface.py +202 -0
- mantisdk/cli/__init__.py +56 -0
- mantisdk/cli/prometheus.py +115 -0
- mantisdk/cli/store.py +131 -0
- mantisdk/cli/vllm.py +29 -0
- mantisdk/client.py +408 -0
- mantisdk/config.py +348 -0
- mantisdk/emitter/__init__.py +43 -0
- mantisdk/emitter/annotation.py +370 -0
- mantisdk/emitter/exception.py +54 -0
- mantisdk/emitter/message.py +61 -0
- mantisdk/emitter/object.py +117 -0
- mantisdk/emitter/reward.py +320 -0
- mantisdk/env_var.py +156 -0
- mantisdk/execution/__init__.py +15 -0
- mantisdk/execution/base.py +64 -0
- mantisdk/execution/client_server.py +443 -0
- mantisdk/execution/events.py +69 -0
- mantisdk/execution/inter_process.py +16 -0
- mantisdk/execution/shared_memory.py +282 -0
- mantisdk/instrumentation/__init__.py +119 -0
- mantisdk/instrumentation/agentops.py +314 -0
- mantisdk/instrumentation/agentops_langchain.py +45 -0
- mantisdk/instrumentation/litellm.py +83 -0
- mantisdk/instrumentation/vllm.py +81 -0
- mantisdk/instrumentation/weave.py +500 -0
- mantisdk/litagent/__init__.py +11 -0
- mantisdk/litagent/decorator.py +536 -0
- mantisdk/litagent/litagent.py +252 -0
- mantisdk/llm_proxy.py +1890 -0
- mantisdk/logging.py +370 -0
- mantisdk/reward.py +7 -0
- mantisdk/runner/__init__.py +11 -0
- mantisdk/runner/agent.py +845 -0
- mantisdk/runner/base.py +182 -0
- mantisdk/runner/legacy.py +309 -0
- mantisdk/semconv.py +170 -0
- mantisdk/server.py +401 -0
- mantisdk/store/__init__.py +23 -0
- mantisdk/store/base.py +897 -0
- mantisdk/store/client_server.py +2092 -0
- mantisdk/store/collection/__init__.py +30 -0
- mantisdk/store/collection/base.py +587 -0
- mantisdk/store/collection/memory.py +970 -0
- mantisdk/store/collection/mongo.py +1412 -0
- mantisdk/store/collection_based.py +1823 -0
- mantisdk/store/insight.py +648 -0
- mantisdk/store/listener.py +58 -0
- mantisdk/store/memory.py +396 -0
- mantisdk/store/mongo.py +165 -0
- mantisdk/store/sqlite.py +3 -0
- mantisdk/store/threading.py +357 -0
- mantisdk/store/utils.py +142 -0
- mantisdk/tracer/__init__.py +16 -0
- mantisdk/tracer/agentops.py +242 -0
- mantisdk/tracer/base.py +287 -0
- mantisdk/tracer/dummy.py +106 -0
- mantisdk/tracer/otel.py +555 -0
- mantisdk/tracer/weave.py +677 -0
- mantisdk/trainer/__init__.py +6 -0
- mantisdk/trainer/init_utils.py +263 -0
- mantisdk/trainer/legacy.py +367 -0
- mantisdk/trainer/registry.py +12 -0
- mantisdk/trainer/trainer.py +618 -0
- mantisdk/types/__init__.py +6 -0
- mantisdk/types/core.py +553 -0
- mantisdk/types/resources.py +204 -0
- mantisdk/types/tracer.py +515 -0
- mantisdk/types/tracing.py +218 -0
- mantisdk/utils/__init__.py +1 -0
- mantisdk/utils/id.py +18 -0
- mantisdk/utils/metrics.py +1025 -0
- mantisdk/utils/otel.py +578 -0
- mantisdk/utils/otlp.py +536 -0
- mantisdk/utils/server_launcher.py +1045 -0
- mantisdk/utils/system_snapshot.py +81 -0
- mantisdk/verl/__init__.py +8 -0
- mantisdk/verl/__main__.py +6 -0
- mantisdk/verl/async_server.py +46 -0
- mantisdk/verl/config.yaml +27 -0
- mantisdk/verl/daemon.py +1154 -0
- mantisdk/verl/dataset.py +44 -0
- mantisdk/verl/entrypoint.py +248 -0
- mantisdk/verl/trainer.py +549 -0
- mantisdk-0.1.0.dist-info/METADATA +119 -0
- mantisdk-0.1.0.dist-info/RECORD +190 -0
- mantisdk-0.1.0.dist-info/WHEEL +4 -0
- mantisdk-0.1.0.dist-info/entry_points.txt +2 -0
- mantisdk-0.1.0.dist-info/licenses/LICENSE +19 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Copyright (c) 2025 Lakshya A Agrawal and the GEPA contributors
|
|
2
|
+
# https://github.com/gepa-ai/gepa
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
from mantisdk.algorithm.gepa.lib.adapters.generic_rag_adapter.vector_store_interface import VectorStoreInterface
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RAGPipeline:
|
|
10
|
+
"""
|
|
11
|
+
Generic RAG pipeline that works with any vector store.
|
|
12
|
+
|
|
13
|
+
This pipeline orchestrates the full RAG process:
|
|
14
|
+
1. Query reformulation (optional)
|
|
15
|
+
2. Document retrieval
|
|
16
|
+
3. Document reranking (optional)
|
|
17
|
+
4. Context synthesis
|
|
18
|
+
5. Answer generation
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
vector_store: VectorStoreInterface,
|
|
24
|
+
llm_client,
|
|
25
|
+
embedding_model: str = "text-embedding-3-small",
|
|
26
|
+
embedding_function: Callable[[str], list[float]] | None = None,
|
|
27
|
+
):
|
|
28
|
+
"""
|
|
29
|
+
Initialize the RAG pipeline.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
vector_store: Vector store interface implementation
|
|
33
|
+
llm_client: LLM client for generation (should have a callable interface)
|
|
34
|
+
embedding_model: Model name for embeddings (if using default embedding function)
|
|
35
|
+
embedding_function: Optional custom embedding function
|
|
36
|
+
"""
|
|
37
|
+
self.vector_store = vector_store
|
|
38
|
+
self.llm_client = llm_client
|
|
39
|
+
self.embedding_model = embedding_model
|
|
40
|
+
self.embedding_function = embedding_function
|
|
41
|
+
|
|
42
|
+
# Initialize default embedding function if none provided
|
|
43
|
+
if self.embedding_function is None:
|
|
44
|
+
self.embedding_function = self._default_embedding_function
|
|
45
|
+
|
|
46
|
+
def execute_rag(
|
|
47
|
+
self,
|
|
48
|
+
query: str,
|
|
49
|
+
prompts: dict[str, str],
|
|
50
|
+
config: dict[str, Any],
|
|
51
|
+
) -> dict[str, Any]:
|
|
52
|
+
"""
|
|
53
|
+
Execute the full RAG pipeline with given prompts and configuration.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
query: User query
|
|
57
|
+
prompts: Dictionary of prompt templates for different stages
|
|
58
|
+
config: Configuration parameters for retrieval and generation
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Dictionary containing all pipeline outputs and metadata
|
|
62
|
+
"""
|
|
63
|
+
# Step 1: Query reformulation (if enabled)
|
|
64
|
+
reformulated_query = query
|
|
65
|
+
if "query_reformulation" in prompts and prompts["query_reformulation"].strip():
|
|
66
|
+
reformulated_query = self._reformulate_query(query, prompts["query_reformulation"])
|
|
67
|
+
|
|
68
|
+
# Step 2: Retrieval
|
|
69
|
+
retrieved_docs = self._retrieve_documents(reformulated_query, config)
|
|
70
|
+
|
|
71
|
+
# Step 3: Reranking (if enabled)
|
|
72
|
+
if "reranking_criteria" in prompts and prompts["reranking_criteria"].strip():
|
|
73
|
+
retrieved_docs = self._rerank_documents(retrieved_docs, query, prompts["reranking_criteria"], config)
|
|
74
|
+
|
|
75
|
+
# Step 4: Context synthesis
|
|
76
|
+
context = self._synthesize_context(retrieved_docs, query, prompts.get("context_synthesis", ""))
|
|
77
|
+
|
|
78
|
+
# Step 5: Answer generation
|
|
79
|
+
answer = self._generate_answer(query, context, prompts.get("answer_generation", ""))
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
"original_query": query,
|
|
83
|
+
"reformulated_query": reformulated_query,
|
|
84
|
+
"retrieved_docs": retrieved_docs,
|
|
85
|
+
"synthesized_context": context,
|
|
86
|
+
"generated_answer": answer,
|
|
87
|
+
"metadata": {
|
|
88
|
+
"retrieval_count": len(retrieved_docs),
|
|
89
|
+
"total_tokens": self._estimate_token_count(context + answer),
|
|
90
|
+
"vector_store_type": self.vector_store.get_collection_info().get("vector_store_type", "unknown"),
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
def _reformulate_query(self, query: str, reformulation_prompt: str) -> str:
|
|
95
|
+
"""Reformulate the user query using the provided prompt."""
|
|
96
|
+
messages = [
|
|
97
|
+
{"role": "system", "content": reformulation_prompt},
|
|
98
|
+
{"role": "user", "content": f"Original query: {query}"},
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
if callable(self.llm_client):
|
|
103
|
+
response = self.llm_client(messages)
|
|
104
|
+
else:
|
|
105
|
+
# Assume it's a litellm-style client
|
|
106
|
+
response = self.llm_client.completion(messages=messages).choices[0].message.content
|
|
107
|
+
|
|
108
|
+
return response.strip() if response else query
|
|
109
|
+
except Exception:
|
|
110
|
+
# Fallback to original query if reformulation fails
|
|
111
|
+
return query
|
|
112
|
+
|
|
113
|
+
def _retrieve_documents(self, query: str, config: dict[str, Any]) -> list[dict[str, Any]]:
|
|
114
|
+
"""Retrieve documents using the configured strategy."""
|
|
115
|
+
retrieval_strategy = config.get("retrieval_strategy", "similarity")
|
|
116
|
+
k = config.get("top_k", 5)
|
|
117
|
+
filters = config.get("filters", None)
|
|
118
|
+
|
|
119
|
+
if retrieval_strategy == "similarity":
|
|
120
|
+
return self.vector_store.similarity_search(query, k=k, filters=filters)
|
|
121
|
+
elif retrieval_strategy == "hybrid":
|
|
122
|
+
if self.vector_store.supports_hybrid_search():
|
|
123
|
+
alpha = config.get("hybrid_alpha", 0.5)
|
|
124
|
+
return self.vector_store.hybrid_search(query, k=k, alpha=alpha, filters=filters)
|
|
125
|
+
else:
|
|
126
|
+
# Fallback to similarity search
|
|
127
|
+
return self.vector_store.similarity_search(query, k=k, filters=filters)
|
|
128
|
+
elif retrieval_strategy == "vector":
|
|
129
|
+
# Use pre-computed embedding
|
|
130
|
+
query_vector = self.embedding_function(query)
|
|
131
|
+
return self.vector_store.vector_search(query_vector, k=k, filters=filters)
|
|
132
|
+
else:
|
|
133
|
+
raise ValueError(f"Unknown retrieval strategy: {retrieval_strategy}")
|
|
134
|
+
|
|
135
|
+
def _rerank_documents(
|
|
136
|
+
self, documents: list[dict[str, Any]], query: str, reranking_prompt: str, config: dict[str, Any]
|
|
137
|
+
) -> list[dict[str, Any]]:
|
|
138
|
+
"""Rerank documents based on relevance criteria."""
|
|
139
|
+
if not documents:
|
|
140
|
+
return documents
|
|
141
|
+
|
|
142
|
+
# For simplicity, we'll use a prompt-based reranking approach
|
|
143
|
+
# In production, you might use dedicated reranking models
|
|
144
|
+
try:
|
|
145
|
+
doc_texts = [f"Document {i + 1}: {doc['content']}" for i, doc in enumerate(documents)]
|
|
146
|
+
doc_context = "\n\n".join(doc_texts)
|
|
147
|
+
|
|
148
|
+
messages = [
|
|
149
|
+
{"role": "system", "content": reranking_prompt},
|
|
150
|
+
{
|
|
151
|
+
"role": "user",
|
|
152
|
+
"content": f"Query: {query}\n\nDocuments:\n{doc_context}\n\nPlease rank these documents by relevance (return document numbers in order, e.g., '3,1,4,2,5'):",
|
|
153
|
+
},
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
if callable(self.llm_client):
|
|
157
|
+
response = self.llm_client(messages)
|
|
158
|
+
else:
|
|
159
|
+
response = self.llm_client.completion(messages=messages).choices[0].message.content
|
|
160
|
+
|
|
161
|
+
# Parse the ranking response
|
|
162
|
+
ranking_str = response.strip()
|
|
163
|
+
rankings = [int(x.strip()) - 1 for x in ranking_str.split(",") if x.strip().isdigit()]
|
|
164
|
+
|
|
165
|
+
# Reorder documents based on ranking
|
|
166
|
+
if len(rankings) == len(documents):
|
|
167
|
+
return [documents[i] for i in rankings if 0 <= i < len(documents)]
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
# Return original order if reranking fails
|
|
172
|
+
return documents
|
|
173
|
+
|
|
174
|
+
def _synthesize_context(self, documents: list[dict[str, Any]], query: str, synthesis_prompt: str) -> str:
|
|
175
|
+
"""Synthesize retrieved documents into coherent context."""
|
|
176
|
+
if not documents:
|
|
177
|
+
return ""
|
|
178
|
+
|
|
179
|
+
if not synthesis_prompt.strip():
|
|
180
|
+
# Default: simple concatenation
|
|
181
|
+
contexts = []
|
|
182
|
+
for i, doc in enumerate(documents):
|
|
183
|
+
contexts.append(f"[Document {i + 1}] {doc['content']}")
|
|
184
|
+
return "\n\n".join(contexts)
|
|
185
|
+
|
|
186
|
+
# Use LLM for context synthesis
|
|
187
|
+
doc_texts = [doc["content"] for doc in documents]
|
|
188
|
+
doc_context = "\n\n".join(f"Document {i + 1}: {text}" for i, text in enumerate(doc_texts))
|
|
189
|
+
|
|
190
|
+
messages = [
|
|
191
|
+
{"role": "system", "content": synthesis_prompt},
|
|
192
|
+
{"role": "user", "content": f"Query: {query}\n\nRetrieved Documents:\n{doc_context}"},
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
if callable(self.llm_client):
|
|
197
|
+
response = self.llm_client(messages)
|
|
198
|
+
else:
|
|
199
|
+
response = self.llm_client.completion(messages=messages).choices[0].message.content
|
|
200
|
+
|
|
201
|
+
return response.strip() if response else doc_context
|
|
202
|
+
except Exception:
|
|
203
|
+
# Fallback to simple concatenation
|
|
204
|
+
return doc_context
|
|
205
|
+
|
|
206
|
+
def _generate_answer(self, query: str, context: str, generation_prompt: str) -> str:
|
|
207
|
+
"""Generate the final answer using the query and synthesized context."""
|
|
208
|
+
if not generation_prompt.strip():
|
|
209
|
+
generation_prompt = "You are a helpful assistant. Answer the user's question based on the provided context."
|
|
210
|
+
|
|
211
|
+
messages = [
|
|
212
|
+
{"role": "system", "content": generation_prompt},
|
|
213
|
+
{"role": "user", "content": f"Context:\n{context}\n\nQuestion: {query}"},
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
if callable(self.llm_client):
|
|
218
|
+
response = self.llm_client(messages)
|
|
219
|
+
else:
|
|
220
|
+
response = self.llm_client.completion(messages=messages).choices[0].message.content
|
|
221
|
+
|
|
222
|
+
return response.strip() if response else "I couldn't generate an answer based on the provided context."
|
|
223
|
+
except Exception as e:
|
|
224
|
+
return f"Error generating answer: {e!s}"
|
|
225
|
+
|
|
226
|
+
def _default_embedding_function(self, text: str) -> list[float]:
|
|
227
|
+
"""Default embedding function using litellm."""
|
|
228
|
+
try:
|
|
229
|
+
import litellm
|
|
230
|
+
|
|
231
|
+
response = litellm.embedding(model=self.embedding_model, input=text)
|
|
232
|
+
return response.data[0].embedding
|
|
233
|
+
except Exception as e:
|
|
234
|
+
raise RuntimeError(f"Failed to generate embeddings: {e!s}") from e
|
|
235
|
+
|
|
236
|
+
def _estimate_token_count(self, text: str) -> int:
|
|
237
|
+
"""Rough estimate of token count (4 chars per token approximation)."""
|
|
238
|
+
return len(text) // 4
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Copyright (c) 2025 Lakshya A Agrawal and the GEPA contributors
|
|
2
|
+
# https://github.com/gepa-ai/gepa
|
|
3
|
+
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VectorStoreInterface(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Abstract interface for vector store operations in RAG systems.
|
|
11
|
+
|
|
12
|
+
This interface defines the core operations needed for retrieval-augmented generation,
|
|
13
|
+
enabling GEPA to work with any vector store implementation (ChromaDB, Weaviate, Qdrant,
|
|
14
|
+
Pinecone, Milvus, etc.) through a unified API.
|
|
15
|
+
|
|
16
|
+
The interface supports:
|
|
17
|
+
- Semantic similarity search
|
|
18
|
+
- Vector-based search with pre-computed embeddings
|
|
19
|
+
- Hybrid search (semantic + keyword, where supported)
|
|
20
|
+
- Metadata filtering and collection introspection
|
|
21
|
+
|
|
22
|
+
Implementing this interface allows your vector store to be used with GEPA's
|
|
23
|
+
evolutionary prompt optimization for RAG systems.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
.. code-block:: python
|
|
27
|
+
|
|
28
|
+
class MyVectorStore(VectorStoreInterface):
|
|
29
|
+
def similarity_search(self, query, k=5, filters=None):
|
|
30
|
+
# Your implementation
|
|
31
|
+
return documents
|
|
32
|
+
|
|
33
|
+
vector_store = MyVectorStore()
|
|
34
|
+
adapter = GenericRAGAdapter(vector_store=vector_store)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def similarity_search(
|
|
39
|
+
self,
|
|
40
|
+
query: str,
|
|
41
|
+
k: int = 5,
|
|
42
|
+
filters: dict[str, Any] | None = None,
|
|
43
|
+
) -> list[dict[str, Any]]:
|
|
44
|
+
"""
|
|
45
|
+
Search for documents semantically similar to the query text.
|
|
46
|
+
|
|
47
|
+
This method performs semantic similarity search using the vector store's
|
|
48
|
+
default embedding model or configured vectorizer.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
query: Text query to search for similar documents
|
|
52
|
+
k: Maximum number of documents to return (default: 5)
|
|
53
|
+
filters: Optional metadata filters to constrain search.
|
|
54
|
+
Format: {"key": "value"} or {"key": {"$op": value}}
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
List of documents ordered by similarity score (highest first).
|
|
58
|
+
Each document is a dictionary with:
|
|
59
|
+
- "content" (str): The document text content
|
|
60
|
+
- "metadata" (dict): Document metadata including any doc_id
|
|
61
|
+
- "score" (float): Similarity score between 0.0 and 1.0 (higher = more similar)
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
NotImplementedError: Must be implemented by concrete vector store classes
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
.. code-block:: python
|
|
68
|
+
|
|
69
|
+
results = vector_store.similarity_search(
|
|
70
|
+
query="machine learning algorithms",
|
|
71
|
+
k=3,
|
|
72
|
+
filters={"category": "AI"}
|
|
73
|
+
)
|
|
74
|
+
print(results[0]["content"]) # Most similar document
|
|
75
|
+
"""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def vector_search(
|
|
80
|
+
self,
|
|
81
|
+
query_vector: list[float],
|
|
82
|
+
k: int = 5,
|
|
83
|
+
filters: dict[str, Any] | None = None,
|
|
84
|
+
) -> list[dict[str, Any]]:
|
|
85
|
+
"""
|
|
86
|
+
Search using a pre-computed query embedding vector.
|
|
87
|
+
|
|
88
|
+
This method allows direct vector similarity search when you already have
|
|
89
|
+
the query embedding, avoiding the need for additional embedding computation.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
query_vector: Pre-computed embedding vector for the query.
|
|
93
|
+
Must match the dimensionality of vectors in the collection.
|
|
94
|
+
k: Maximum number of documents to return (default: 5)
|
|
95
|
+
filters: Optional metadata filters to constrain search
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
List of documents ordered by vector similarity (highest first).
|
|
99
|
+
Same format as similarity_search().
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
NotImplementedError: Must be implemented by concrete vector store classes
|
|
103
|
+
ValueError: If query_vector dimensions don't match collection
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
.. code-block:: python
|
|
107
|
+
|
|
108
|
+
import numpy as np
|
|
109
|
+
query_vector = embedding_model.encode("machine learning")
|
|
110
|
+
results = vector_store.vector_search(query_vector.tolist(), k=5)
|
|
111
|
+
"""
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
def hybrid_search(
|
|
115
|
+
self,
|
|
116
|
+
query: str,
|
|
117
|
+
k: int = 5,
|
|
118
|
+
alpha: float = 0.5,
|
|
119
|
+
filters: dict[str, Any] | None = None,
|
|
120
|
+
) -> list[dict[str, Any]]:
|
|
121
|
+
"""
|
|
122
|
+
Hybrid semantic + keyword search combining vector and text-based matching.
|
|
123
|
+
|
|
124
|
+
This method combines semantic similarity (vector search) with keyword-based
|
|
125
|
+
search (like BM25) to leverage both approaches. The alpha parameter controls
|
|
126
|
+
the balance between the two search methods.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
query: Text query to search for
|
|
130
|
+
k: Maximum number of documents to return (default: 5)
|
|
131
|
+
alpha: Weight for semantic vs keyword search (default: 0.5)
|
|
132
|
+
- 0.0 = pure keyword/BM25 search
|
|
133
|
+
- 1.0 = pure semantic/vector search
|
|
134
|
+
- 0.5 = balanced hybrid search
|
|
135
|
+
filters: Optional metadata filters to constrain search
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of documents ordered by hybrid similarity score (highest first).
|
|
139
|
+
Same format as similarity_search().
|
|
140
|
+
|
|
141
|
+
Note:
|
|
142
|
+
If hybrid search is not supported by the vector store implementation,
|
|
143
|
+
this method falls back to similarity_search() with a warning.
|
|
144
|
+
Use supports_hybrid_search() to check availability.
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
.. code-block:: python
|
|
148
|
+
|
|
149
|
+
# Balanced hybrid search
|
|
150
|
+
results = vector_store.hybrid_search("AI algorithms", alpha=0.5)
|
|
151
|
+
|
|
152
|
+
# More semantic-focused
|
|
153
|
+
results = vector_store.hybrid_search("AI algorithms", alpha=0.8)
|
|
154
|
+
"""
|
|
155
|
+
# Default fallback implementation
|
|
156
|
+
return self.similarity_search(query, k, filters)
|
|
157
|
+
|
|
158
|
+
@abstractmethod
|
|
159
|
+
def get_collection_info(self) -> dict[str, Any]:
|
|
160
|
+
"""
|
|
161
|
+
Get metadata and statistics about the vector store collection.
|
|
162
|
+
|
|
163
|
+
This method provides introspection capabilities for the collection,
|
|
164
|
+
returning key information about its configuration and contents.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Dictionary containing collection metadata with keys:
|
|
168
|
+
- "name" (str): Collection/index name
|
|
169
|
+
- "document_count" (int): Total number of documents
|
|
170
|
+
- "dimension" (int): Vector embedding dimension (0 if unknown)
|
|
171
|
+
- "vector_store_type" (str): Type of vector store (e.g., "chromadb", "weaviate")
|
|
172
|
+
- Additional store-specific metadata as available
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
NotImplementedError: Must be implemented by concrete vector store classes
|
|
176
|
+
|
|
177
|
+
Example:
|
|
178
|
+
.. code-block:: python
|
|
179
|
+
|
|
180
|
+
info = vector_store.get_collection_info()
|
|
181
|
+
print(f"Collection {info['name']} has {info['document_count']} documents")
|
|
182
|
+
print(f"Vector dimension: {info['dimension']}")
|
|
183
|
+
"""
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
def get_embedding_dimension(self) -> int:
|
|
187
|
+
"""
|
|
188
|
+
Get the embedding dimension of the vector store.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Dimension of the vectors in the collection
|
|
192
|
+
"""
|
|
193
|
+
info = self.get_collection_info()
|
|
194
|
+
return info.get("dimension", 0)
|
|
195
|
+
|
|
196
|
+
def supports_hybrid_search(self) -> bool:
|
|
197
|
+
"""
|
|
198
|
+
Check if the vector store supports hybrid search.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
True if hybrid search is supported, False otherwise
|
|
202
|
+
"""
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
def supports_metadata_filtering(self) -> bool:
|
|
206
|
+
"""
|
|
207
|
+
Check if the vector store supports metadata filtering.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
True if metadata filtering is supported, False otherwise
|
|
211
|
+
"""
|
|
212
|
+
return True
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Copyright (c) 2025 Lakshya A Agrawal and the GEPA contributors
|
|
2
|
+
# https://github.com/gepa-ai/gepa
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from mantisdk.algorithm.gepa.lib.adapters.generic_rag_adapter.vector_store_interface import VectorStoreInterface
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ChromaVectorStore(VectorStoreInterface):
|
|
10
|
+
"""
|
|
11
|
+
ChromaDB implementation of the VectorStoreInterface.
|
|
12
|
+
|
|
13
|
+
ChromaDB is an open-source embedding database that's easy to use
|
|
14
|
+
and perfect for local development and prototyping.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, client, collection_name: str, embedding_function=None):
|
|
18
|
+
"""
|
|
19
|
+
Initialize ChromaVectorStore.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
client: ChromaDB client instance
|
|
23
|
+
collection_name: Name of the collection to use
|
|
24
|
+
embedding_function: Optional embedding function for text queries
|
|
25
|
+
"""
|
|
26
|
+
import importlib.util
|
|
27
|
+
|
|
28
|
+
if importlib.util.find_spec("chromadb") is None:
|
|
29
|
+
raise ImportError("ChromaDB is required for ChromaVectorStore. Install with: pip install litellm chromadb")
|
|
30
|
+
|
|
31
|
+
self.client = client
|
|
32
|
+
self.collection_name = collection_name
|
|
33
|
+
self.embedding_function = embedding_function
|
|
34
|
+
|
|
35
|
+
# Get or create the collection
|
|
36
|
+
try:
|
|
37
|
+
self.collection = self.client.get_collection(name=collection_name, embedding_function=embedding_function)
|
|
38
|
+
except Exception:
|
|
39
|
+
# Collection doesn't exist, create it
|
|
40
|
+
self.collection = self.client.create_collection(name=collection_name, embedding_function=embedding_function)
|
|
41
|
+
|
|
42
|
+
def similarity_search(
|
|
43
|
+
self,
|
|
44
|
+
query: str,
|
|
45
|
+
k: int = 5,
|
|
46
|
+
filters: dict[str, Any] | None = None,
|
|
47
|
+
) -> list[dict[str, Any]]:
|
|
48
|
+
"""Search for documents similar to the query text."""
|
|
49
|
+
# Convert filters to ChromaDB format if provided
|
|
50
|
+
where_clause = self._convert_filters(filters) if filters else None
|
|
51
|
+
|
|
52
|
+
results = self.collection.query(
|
|
53
|
+
query_texts=[query], n_results=k, where=where_clause, include=["documents", "metadatas", "distances"]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return self._format_results(results)
|
|
57
|
+
|
|
58
|
+
def vector_search(
|
|
59
|
+
self,
|
|
60
|
+
query_vector: list[float],
|
|
61
|
+
k: int = 5,
|
|
62
|
+
filters: dict[str, Any] | None = None,
|
|
63
|
+
) -> list[dict[str, Any]]:
|
|
64
|
+
"""Search using a pre-computed query vector."""
|
|
65
|
+
where_clause = self._convert_filters(filters) if filters else None
|
|
66
|
+
|
|
67
|
+
results = self.collection.query(
|
|
68
|
+
query_embeddings=[query_vector],
|
|
69
|
+
n_results=k,
|
|
70
|
+
where=where_clause,
|
|
71
|
+
include=["documents", "metadatas", "distances"],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return self._format_results(results)
|
|
75
|
+
|
|
76
|
+
def get_collection_info(self) -> dict[str, Any]:
|
|
77
|
+
"""Get metadata about the ChromaDB collection."""
|
|
78
|
+
count = self.collection.count()
|
|
79
|
+
|
|
80
|
+
# Try to get a sample document to determine embedding dimension
|
|
81
|
+
dimension = 0
|
|
82
|
+
if count > 0:
|
|
83
|
+
sample = self.collection.peek(limit=1)
|
|
84
|
+
embeddings = sample.get("embeddings")
|
|
85
|
+
if embeddings is not None and len(embeddings) > 0:
|
|
86
|
+
dimension = len(embeddings[0])
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"name": self.collection_name,
|
|
90
|
+
"document_count": count,
|
|
91
|
+
"dimension": dimension,
|
|
92
|
+
"vector_store_type": "chromadb",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def supports_metadata_filtering(self) -> bool:
|
|
96
|
+
"""ChromaDB supports metadata filtering."""
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
def _convert_filters(self, filters: dict[str, Any]) -> dict[str, Any]:
|
|
100
|
+
"""
|
|
101
|
+
Convert generic filters to ChromaDB where clause format.
|
|
102
|
+
|
|
103
|
+
Generic format: {"key": "value", "key2": {"$gt": 5}}
|
|
104
|
+
ChromaDB format: {"key": {"$eq": "value"}, "key2": {"$gt": 5}}
|
|
105
|
+
"""
|
|
106
|
+
chroma_filters = {}
|
|
107
|
+
|
|
108
|
+
for key, value in filters.items():
|
|
109
|
+
if isinstance(value, dict):
|
|
110
|
+
# Already in operator format
|
|
111
|
+
chroma_filters[key] = value
|
|
112
|
+
else:
|
|
113
|
+
# Convert to equality operator
|
|
114
|
+
chroma_filters[key] = {"$eq": value}
|
|
115
|
+
|
|
116
|
+
return chroma_filters
|
|
117
|
+
|
|
118
|
+
def _format_results(self, results) -> list[dict[str, Any]]:
|
|
119
|
+
"""Convert ChromaDB results to standardized format."""
|
|
120
|
+
documents = []
|
|
121
|
+
|
|
122
|
+
if not results["documents"] or not results["documents"][0]:
|
|
123
|
+
return documents
|
|
124
|
+
|
|
125
|
+
docs = results["documents"][0]
|
|
126
|
+
metadatas = results.get("metadatas", [None] * len(docs))[0] or [{}] * len(docs)
|
|
127
|
+
distances = results.get("distances", [0.0] * len(docs))[0]
|
|
128
|
+
|
|
129
|
+
for doc, metadata, distance in zip(docs, metadatas, distances, strict=False):
|
|
130
|
+
# Convert distance to similarity score (higher is better)
|
|
131
|
+
# ChromaDB uses cosine distance, so similarity = 1 - distance
|
|
132
|
+
distance_val = self._extract_distance_value(distance)
|
|
133
|
+
similarity_score = max(0.0, 1.0 - distance_val)
|
|
134
|
+
|
|
135
|
+
documents.append({"content": doc, "metadata": metadata or {}, "score": similarity_score})
|
|
136
|
+
|
|
137
|
+
return documents
|
|
138
|
+
|
|
139
|
+
def _extract_distance_value(self, distance) -> float:
|
|
140
|
+
"""
|
|
141
|
+
Helper to extract a scalar float from a distance value returned by ChromaDB.
|
|
142
|
+
Handles numpy scalars, single-element lists/arrays, and plain floats.
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
if hasattr(distance, "item"): # numpy scalar
|
|
146
|
+
return distance.item()
|
|
147
|
+
elif hasattr(distance, "__len__") and not isinstance(distance, str | bytes) and len(distance) == 1:
|
|
148
|
+
return float(distance[0])
|
|
149
|
+
else:
|
|
150
|
+
return float(distance)
|
|
151
|
+
except (TypeError, ValueError, IndexError) as e:
|
|
152
|
+
import logging
|
|
153
|
+
|
|
154
|
+
logging.warning(f"Unexpected distance format: {type(distance)}, value: {distance}, error: {e}")
|
|
155
|
+
return 0.0 # Default fallback
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def create_local(cls, persist_directory: str, collection_name: str, embedding_function=None) -> "ChromaVectorStore":
|
|
159
|
+
"""
|
|
160
|
+
Create a ChromaVectorStore with local persistence.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
persist_directory: Directory to persist the database
|
|
164
|
+
collection_name: Name of the collection
|
|
165
|
+
embedding_function: Optional embedding function
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
ChromaVectorStore instance
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
import chromadb
|
|
172
|
+
except ImportError as e:
|
|
173
|
+
raise ImportError("ChromaDB is required. Install with: pip install litellm chromadb") from e
|
|
174
|
+
|
|
175
|
+
client = chromadb.PersistentClient(path=persist_directory)
|
|
176
|
+
return cls(client, collection_name, embedding_function)
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def create_memory(cls, collection_name: str, embedding_function=None) -> "ChromaVectorStore":
|
|
180
|
+
"""
|
|
181
|
+
Create a ChromaVectorStore in memory (for testing).
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
collection_name: Name of the collection
|
|
185
|
+
embedding_function: Optional embedding function
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
ChromaVectorStore instance
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
import chromadb
|
|
192
|
+
except ImportError as e:
|
|
193
|
+
raise ImportError("ChromaDB is required. Install with: pip install litellm chromadb") from e
|
|
194
|
+
|
|
195
|
+
client = chromadb.Client()
|
|
196
|
+
return cls(client, collection_name, embedding_function)
|