Skip to content

Commit f3d63e4

Browse files
authored
Tools update (#21)
neumai 0.0.33 - Small updates to website connector - Addition of vector as parameters on search result (only added for Weaviate, but other should be trivial) - Pipeline description field to be used for routed based search neumai-tool 0.0.17 - PipelineCollections - Dataset evaluation - Interop helpers - Update of semantic helpers
1 parent b101464 commit f3d63e4

19 files changed

Lines changed: 317 additions & 49 deletions

File tree

neumai-tools/README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
11
# Neum AI Tools
22

3-
Collection of tools made to help implement RAG pipelines. Can be used directly or through Neum AI.
3+
Collection of tools made to help implement RAG pipelines. Can be used directly or through Neum AI.
4+
5+
| :exclamation: This utilities are currently in experimental phase. |
6+
|----------------------------------------------------------------------|
7+
8+
## Tools
9+
10+
- [Semantic Helpers](./neumai_tools/SemanticHelpers/): LLM based tools to help augment RAG pipelines. Includes generating semantic strategies to generate chunking code, select fields to capture as metadata and as content when leveraging RAG on top of structured data.
11+
- [Interop Helpers](./neumai_tools/InteropHelpers/): Utilities to connect frameworks like Langchain and Llama Index with Neum AI. If there are data connectors that you want to leverage, you can use the utilities to translate interfaces.
12+
- [Pipeline Collection](./neumai_tools/PipelineCollection/): Treat a collection of pipelines as a single entity to perform search and other actions. Allows you to silo data into pipelines with their unique set of transformations, embed model and sink and then bring them back together as a single entity to retrieve data.
13+
- [Dataset Evaluation](./neumai_tools/DatasetEvaluation/): Create datasets of queries and expected outputs that you can run against a given pipeline or pipeline collection.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from pydantic import BaseModel, Field
2+
from neumai.Pipelines.Pipeline import Pipeline
3+
from neumai_tools.PipelineCollection.PipelineCollection import PipelineCollection
4+
from .Evaluation import Evaluation, CosineEvaluation
5+
from .DatasetUtils import DatasetEntry, DatasetResult, DatasetResults
6+
from uuid import uuid4
7+
from typing import List, Type
8+
import numpy as np
9+
10+
class Dataset(BaseModel):
11+
12+
name : str = Field(... , description="")
13+
dataset_entries : List[DatasetEntry] = Field(... , description="")
14+
evaluation_type: Type[Evaluation] = Field(...,description="Evaluation type for results.")
15+
16+
def run_with_pipeline(self, pipeline: Pipeline) -> DatasetResults:
17+
18+
dataset_results = DatasetResults()
19+
20+
for dataset_entry in self.dataset_entries:
21+
# Generate results
22+
result_output = pipeline.search(query=dataset_entry.query, number_of_results=1)[0]
23+
24+
dataset_result = self.evaluation_type(pipeline=pipeline, dataset_entry=dataset_entry, result_output=result_output).evaluate()
25+
26+
dataset_results.dataset_results.append(dataset_result)
27+
28+
return dataset_results
29+
30+
31+
def run_with_pipeline_collection_unified(self, pipeline_collection: PipelineCollection):
32+
dataset_results = DatasetResults()
33+
34+
for dataset_entry in self.dataset_entries:
35+
# Generate results
36+
result_output = pipeline_collection.search_unified(query=dataset_entry.query, number_of_results=1)[0]
37+
38+
dataset_result = self.evaluation_type(pipeline=pipeline_collection.pipelines[0], dataset_entry=dataset_entry, result_output=result_output).evaluate()
39+
40+
dataset_results.dataset_results.append(dataset_result)
41+
42+
return dataset_results
43+
44+
def run_with_pipeline_collection_separate(self, pipeline_collection: PipelineCollection):
45+
dataset_results_separate: dict[str,DatasetResults] = {}
46+
for pipeline in pipeline_collection.pipelines:
47+
dataset_results = DatasetResults()
48+
49+
for dataset_entry in self.dataset_entries:
50+
# Generate results
51+
result_output = pipeline.search(query=dataset_entry.query, number_of_results=1)[0]
52+
53+
dataset_result = self.evaluation_type(pipeline=pipeline, dataset_entry=dataset_entry, result_output=result_output).evaluate()
54+
55+
dataset_results.dataset_results.append(dataset_result)
56+
57+
dataset_results_separate[pipeline.id] = dataset_results
58+
59+
return dataset_results_separate
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from pydantic import BaseModel, Field
2+
from uuid import uuid4
3+
from neumai.Shared.NeumSearch import NeumSearchResult
4+
from typing import List, Optional
5+
6+
class DatasetEntry(BaseModel):
7+
8+
id : str = Field(uuid4() , description="")
9+
query : str = Field(... , description="")
10+
expected_output : str = Field(... , description="")
11+
12+
class DatasetResult(BaseModel):
13+
14+
dataset_entry : DatasetEntry = Field(... , description="")
15+
raw_result : NeumSearchResult = Field(... , description="")
16+
score : Optional[float] = Field(None, description="")
17+
evaluation: Optional[dict] = Field(None, description="")
18+
19+
class DatasetResults(BaseModel):
20+
dataset_results_id : str = Field(uuid4(), description="")
21+
dataset_results : List[DatasetResult] = Field([], description="")
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
from pydantic import BaseModel, Field
2+
from abc import ABC, abstractmethod
3+
from neumai.Pipelines.Pipeline import Pipeline
4+
from .DatasetUtils import DatasetResult, DatasetEntry
5+
from neumai.Shared.NeumSearch import NeumSearchResult
6+
import numpy as np
7+
import json
8+
import re
9+
10+
class Evaluation(BaseModel, ABC):
11+
pipeline: Pipeline = Field(...,description="")
12+
dataset_entry: DatasetEntry = Field(...,description="Dataset entry for evaluation")
13+
result_output: NeumSearchResult = Field(..., description="Actual result for retrieval")
14+
15+
@abstractmethod
16+
def evaluate(self) -> DatasetResult:
17+
"""Method that runs evaluation and generates results"""
18+
19+
class CosineEvaluation(Evaluation):
20+
21+
def evaluate(self) -> DatasetResult:
22+
# Calculate score -> cosine similarity between expected output and result
23+
24+
# Calculate vectors for both outputs
25+
expected_output_vector = self.pipeline.embed.embed_query(query=self.dataset_entry.expected_output)
26+
result_vector = self.pipeline.embed.embed_query(query=self.result_output.metadata['text'])
27+
28+
# Normalize the vectors to unit vectors
29+
expected_output_vector_norm = expected_output_vector / np.linalg.norm(expected_output_vector)
30+
result_vector_norm = result_vector / np.linalg.norm(result_vector)
31+
32+
# Calculate the dot product and return
33+
similarity = np.dot(expected_output_vector_norm, result_vector_norm)
34+
35+
dataset_result = DatasetResult(
36+
dataset_entry=self.dataset_entry,
37+
raw_result=self.result_output,
38+
score=similarity
39+
)
40+
41+
return dataset_result
42+
43+
class LLMEvaluation(Evaluation):
44+
45+
def evaluate(self) -> DatasetResult:
46+
system_prompt = """
47+
Evaluate the following questions and pieces of context.
48+
49+
Question: Was ketchup originally a type of medicine?
50+
Expected output: Yes, in the 1830's ketchup was sold as a medicine.
51+
Actual output: Ketchup was sold in the 1830s as medicine. In 1834, it was sold as a cure for an upset stomach by an Ohio physician named John Cook. It wasn't popularized as a condiment until the late 19th century!
52+
Evaluation: {
53+
"relevant_context": true,
54+
"all_context_present": true
55+
}
56+
57+
Question: Where did the shortest war in history happen?
58+
Expected output: The war took place in Zanzibar.
59+
Actual output: The shortest war in history lasted 38 minutes! It was between Britain and Zanzibar and is known as the Anglo-Zanzibar War. This war occurred on August 27, 1896. It was over the ascension of the next Sultan in Zanzibar and resulted in a British victory.
60+
Evaluation: {
61+
"relevant_context": true,
62+
"all_context_present": false
63+
}
64+
65+
Question: What animals did Roman's have as pets?
66+
Expected output: Ferrets, dogs and monkeys were the most popular pets in the Roman Empire.
67+
Actual output: Roman's didn't have cats as pets.
68+
Evaluation: {
69+
"relevant_context": false,
70+
"all_context_present": false
71+
}
72+
"""
73+
74+
75+
user_prompt = """Question: {question}
76+
Expected output: {expected_output}
77+
Actual output: {actual_output}
78+
Evaluation:
79+
"""
80+
81+
generated_user_prompt = user_prompt.format(
82+
question=self.dataset_entry.query,
83+
expected_output = self.dataset_entry.expected_output,
84+
actual_output = self.result_output.metadata['text']
85+
)
86+
87+
from openai import OpenAI
88+
client = OpenAI()
89+
90+
response = client.chat.completions.create(
91+
model="gpt-3.5-turbo",
92+
messages=[
93+
{
94+
"role": "system",
95+
"content": system_prompt
96+
},
97+
{
98+
"role":"user",
99+
"content":generated_user_prompt
100+
}
101+
],
102+
temperature=1,
103+
)
104+
105+
match = re.search(r'\{.*?\}', response.choices[0].message.content, re.DOTALL)
106+
107+
if match:
108+
json_object = match.group()
109+
# Now, json_object contains the JSON string.
110+
# To convert it to a Python dictionary, use json.loads if it's a valid JSON
111+
try:
112+
evaluation = json.loads(json_object)
113+
# data is now a Python dictionary representing your JSON object
114+
except json.JSONDecodeError:
115+
print("The extracted string is not a valid JSON.")
116+
evaluation = None
117+
else:
118+
print("No JSON object found in the string.")
119+
evaluation = None
120+
121+
return DatasetResult(
122+
dataset_entry=self.dataset_entry,
123+
raw_result=self.result_output,
124+
evaluation=evaluation
125+
)

neumai-tools/neumai_tools/DatasetEvaluation/__init__.py

Whitespace-only changes.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from langchain.docstore.document import Document as LangChainDocument
2+
from llama_index import Document as LlamaIndexDocument
3+
from neumai.Shared.NeumDocument import NeumDocument
4+
from uuid import uuid4
5+
6+
def document_transformer_langchain(document:LangChainDocument, id:str = uuid4()):
7+
return NeumDocument(
8+
id=id,
9+
content=document.page_content,
10+
metadata=document.metadata
11+
)
12+
13+
def document_transformer_llamaIndex(document:LlamaIndexDocument):
14+
return NeumDocument(
15+
id=document.doc_id,
16+
content=document.text,
17+
metadata=document.metadata
18+
)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .DocumentTransformer import (
2+
document_transformer_langchain,
3+
document_transformer_llamaIndex
4+
)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from typing import List
2+
from pydantic import BaseModel, Field
3+
from neumai.Pipelines.Pipeline import Pipeline
4+
from neumai.Shared.NeumSearch import NeumSearchResult
5+
6+
class PipelineCollection(BaseModel):
7+
"""
8+
Pipeline Collection
9+
10+
Collections of Pipelines that can be managed as a unit. Includes running and searching them as a unit.
11+
12+
Attributes
13+
----------
14+
15+
pipelines : List[Pipeline]
16+
List of Neum AI Pipelines
17+
18+
"""
19+
20+
pipelines: List[Pipeline] = Field(..., description = "Pipelines that are to be par of the collection.")
21+
22+
def run(self):
23+
for pipeline in self.pipelines:
24+
pipeline.run()
25+
26+
def search_unified(self, query:str, number_of_results:int) -> List[NeumSearchResult]:
27+
"""Search pipelines and unify results using scores as re-ranking criteria"""
28+
29+
# Simple query of results
30+
search_results = []
31+
for pipeline in self.pipelines:
32+
results = pipeline.search(query=query, number_of_results=number_of_results)
33+
search_results.append(results)
34+
35+
# Re-rank results by score. Should add a callback option for re-rank
36+
return sorted(search_results, key=lambda x: x.score, reverse=True)[:number_of_results]
37+
38+
def search_separate(self, query:str, number_of_results:int)-> List:
39+
"""Search pipelines and provide raw results for each pipeline"""
40+
41+
# Simple query of results
42+
search_results = []
43+
for pipeline in self.pipelines:
44+
results = pipeline.search(query=query, number_of_results=number_of_results)
45+
search_results.append({
46+
"pipeline_id":pipeline.id,
47+
"results" : results
48+
})
49+
50+
# Re-rank results by score. Should add a callback option for re-rank
51+
return (search_results)
52+
53+
def search_routed(self, query:str, number_of_results:int)-> List:
54+
"""Routed search based on the contents available in a pipeline"""
55+
# Need to add descriptions to the pipeline and generate a basic index on top of them
56+
raise NotImplementedError("In the works. Contact founders@tryneum.com for information")

neumai-tools/neumai_tools/PipelineCollection/__init__.py

Whitespace-only changes.

neumai-tools/neumai_tools/SemanticHelpers/semantic_chunking.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import openai
2-
from langchain.docstore.document import Document
2+
from neumai.Shared.NeumDocument import NeumDocument
33
from typing import (
44
List,
55
)
@@ -32,7 +32,7 @@ def semantic_chunking_strategy_code(text:str, chunking_strategy:str) -> str:
3232
messages = [
3333
{"role": "system", "content": ('You are helpful developer that writes python code.' +
3434
'Output the code in this format: ```python def split_text_into_chunks(text): <Insert Code>```' +
35-
'The function `split_text_into_chunks` should output an array of chunks.'
35+
'The function `split_text_into_chunks` should output an array of text chunks.'
3636
'Implement the strategy provided by the user to help split text.')},
3737
{"role": "user", "content": chunking_strategy}
3838
]
@@ -53,13 +53,13 @@ def semantic_chunking_code(text:str) -> str:
5353
chunking_code_exec = chunking_code.split("```python")[1].split("```")[0]
5454
return chunking_code_exec
5555

56-
def semantic_chunking(documents:List[Document], chunking_code_exec: str) -> List[Document]:
56+
def semantic_chunking(documents:List[NeumDocument], chunking_code_exec: str) -> List[NeumDocument]:
5757
exec(chunking_code_exec, globals())
5858
result_doc = []
5959
for doc in documents:
6060
results = split_text_into_chunks(doc.page_content)
6161
for result in results:
62-
result_doc.append(Document(page_content=result, metadata=doc.metadata))
62+
result_doc.append(NeumDocument(id=doc.id, content=result, metadata=doc.metadata))
6363
return result_doc
6464

6565

0 commit comments

Comments
 (0)