veri-agents-knowledgebase 0.1.1__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.
@@ -0,0 +1 @@
1
+ from .knowledgebase import DocumentLoader, KnowledgeFilter, DataSource, KnowledgebaseMetadata, Knowledgebase, RWKnowledgebase
@@ -0,0 +1,2 @@
1
+ from .qa import create_qa_agent
2
+ from .extract import create_extract_agent
@@ -0,0 +1,213 @@
1
+ import logging
2
+ from typing import Annotated, Callable, List, Optional, Sequence, TypedDict, cast
3
+
4
+ from langchain_core.language_models import BaseLanguageModel
5
+ from langchain_core.messages import (
6
+ AIMessage,
7
+ AnyMessage,
8
+ HumanMessage,
9
+ SystemMessage,
10
+ ToolMessage,
11
+ )
12
+ from langchain_core.runnables.config import RunnableConfig
13
+ from langgraph.graph import END, START, StateGraph
14
+ from langgraph.graph.graph import CompiledGraph
15
+ from langgraph.graph.message import add_messages
16
+ from pydantic import BaseModel, Field
17
+ from veri_agents_knowledgebase import Knowledgebase, KnowledgeFilter
18
+ from veri_agents_knowledgebase.utils import get_filter_from_config, aprocess_docs
19
+
20
+ log = logging.getLogger(__name__)
21
+ log.setLevel(logging.DEBUG)
22
+
23
+ Entities = dict[str, list[str]]
24
+ EntitiesPerDocument = dict[str, Entities]
25
+
26
+
27
+ class ExtractionResponse(BaseModel):
28
+ """Extracted information from documents"""
29
+
30
+ entities: Entities = Field(
31
+ description="Entities extracted from the documents. If you can't find any occurrences of the requested entity, return an empty list for that entity. DO NOT call this tool with the provided examples. Don't return multiple variants of the same entity like 'employee' and 'employees'. Type of field is JSON object with keys corresponding to entity type, and values of entity name with type string. NOT A STRING",
32
+ examples=[
33
+ {
34
+ "persons": [
35
+ "John Doe",
36
+ "Jane James",
37
+ "S1 (Construction Superintendent)",
38
+ ],
39
+ "objects": ["excavator", "dog"],
40
+ },
41
+ {
42
+ "locations": ["New York", "Los Angeles"],
43
+ "dates": ["2025-02-05", "2025-03-10"],
44
+ },
45
+ ],
46
+ )
47
+
48
+
49
+ def reduce_extraction_responses(
50
+ existing: Entities | None, new: ExtractionResponse
51
+ ) -> Entities:
52
+ """Reduce two extraction responses by merging their entities."""
53
+ if existing is None:
54
+ existing = Entities()
55
+ for entity_type, entities in new.entities.items():
56
+ if entity_type not in existing:
57
+ existing[entity_type] = []
58
+ # Add only new entities (avoid duplicates)
59
+ for entity in entities:
60
+ if entity not in existing[entity_type]:
61
+ existing[entity_type].append(entity)
62
+ return existing
63
+
64
+
65
+ class ExtractInputSchema(BaseModel):
66
+ """Input schema for the extract workflow"""
67
+
68
+ knowledgebase: Optional[str] = Field(
69
+ default=None,
70
+ description="Knowledgebase to extract from",
71
+ examples=["knowledgebase1", "knowledgebase2"],
72
+ )
73
+ documents: List[str] = Field(
74
+ default=[],
75
+ description="Extract just within the list of documents with the given IDs.",
76
+ examples=[["source1", "source2"]],
77
+ )
78
+ tags_any: List[str] = Field(
79
+ default=[],
80
+ description="Extract entities from documents matching any of the given tags.",
81
+ examples=[["finance", "priority_high"]],
82
+ )
83
+ tags_all: List[str] = Field(
84
+ default=[],
85
+ description="Extract entities from documents matching all of the given tags.",
86
+ examples=[["finance", "priority_high"]],
87
+ )
88
+ llm: Optional[str] = Field(
89
+ default=None,
90
+ description="LLM to use for extraction",
91
+ examples=["nova_pro", "bedrock_claude_sonnet_37"],
92
+ )
93
+
94
+
95
+ class ExtractOutputSchema(BaseModel):
96
+ """Output schema for the extract workflow"""
97
+
98
+ content: str = Field(
99
+ description="Short summary of the entities",
100
+ examples=[
101
+ "The following entities were extracted: John Doe, Jane James, excavator"
102
+ ],
103
+ )
104
+ entities: EntitiesPerDocument = Field(
105
+ default={},
106
+ description="Extracted entities. Dictionary of documents to entities where entities are a dictionary of entity types to list of entities",
107
+ )
108
+
109
+
110
+ class AgentState(TypedDict):
111
+ messages: Annotated[Sequence[AnyMessage], add_messages]
112
+ documents: EntitiesPerDocument
113
+
114
+
115
+ def create_extract_agent(
116
+ extract_llm: BaseLanguageModel | Callable[[RunnableConfig], BaseLanguageModel],
117
+ knowledgebase: Knowledgebase | Callable[[RunnableConfig], Knowledgebase],
118
+ filter: KnowledgeFilter | None = None,
119
+ system_prompt: str | None = None,
120
+ summarize_prompt: str | None = None,
121
+ ) -> CompiledGraph:
122
+ if system_prompt is None:
123
+ system_prompt = "Your job is to identify important entities, aspects, key points, incidents etc. in the provided data. only use the provided context, don't use your internal knowledge to make up things."
124
+ if summarize_prompt is None:
125
+ summarize_prompt = "Summarize your findings about the extracted entities from the documents: \n"
126
+
127
+ async def aextract(state: AgentState, config: RunnableConfig) -> AgentState:
128
+ """LangGraph node to extract entities from the documents"""
129
+ try:
130
+ messages = state["messages"]
131
+ runnable_filter = get_filter_from_config(config, filter, prefix="filter_")
132
+ print("EXTRACT RUNNABLE FILTER")
133
+ print(runnable_filter)
134
+ runnable_kb = knowledgebase(config) if callable(knowledgebase) else knowledgebase
135
+ runnable_llm = extract_llm(config) if not isinstance(extract_llm, BaseLanguageModel) else extract_llm
136
+ docs = runnable_kb.get_documents(runnable_filter)
137
+ llm_structured = runnable_llm.with_structured_output(ExtractionResponse)
138
+
139
+ extraction_results = await aprocess_docs(
140
+ docs,
141
+ messages,
142
+ llm_structured,
143
+ system_prompt=system_prompt,
144
+ reduce_results=reduce_extraction_responses,
145
+ )
146
+ extraction_results = cast(EntitiesPerDocument, extraction_results)
147
+ artifact = {
148
+ "type": "json",
149
+ "source": "extracted_entities",
150
+ "documents": extraction_results,
151
+ }
152
+ return {
153
+ "messages": [
154
+ ToolMessage(
155
+ "Extraction complete.",
156
+ tool_call_id="extract",
157
+ artifact=artifact,
158
+ )
159
+ ],
160
+ "documents": extraction_results,
161
+ }
162
+ except Exception as e:
163
+ log.error("Error in extract: ", exc_info=True)
164
+ return {
165
+ "messages": [AIMessage(content="Error in extraction: " + str(e))],
166
+ "documents": {},
167
+ }
168
+
169
+ async def asummarize(state: AgentState, config: RunnableConfig) -> AgentState:
170
+ """LangGraph node to summarize the extracted entities from previous nodes."""
171
+ runnable_llm = extract_llm(config) if not isinstance(extract_llm, BaseLanguageModel) else extract_llm
172
+ try:
173
+ messages = state["messages"]
174
+ send_messages = [
175
+ SystemMessage(content=system_prompt),
176
+ messages[-2], # the user input
177
+ HumanMessage(content=summarize_prompt + str(state["documents"])),
178
+ ]
179
+
180
+ response = await runnable_llm.ainvoke(send_messages)
181
+ output = ExtractOutputSchema(
182
+ content=response.content,
183
+ entities=state["documents"],
184
+ )
185
+
186
+ return {
187
+ "messages": [
188
+ AIMessage(
189
+ content=output.model_dump_json(),
190
+ )
191
+ ],
192
+ "documents": state["documents"],
193
+ }
194
+ except Exception as e:
195
+ log.error("Error in summarize: ", exc_info=True)
196
+ output = ExtractOutputSchema(
197
+ content="Error in summarization: " + str(e),
198
+ entities=state["documents"],
199
+ )
200
+ return {
201
+ "messages": [AIMessage(content=output.model_dump_json())],
202
+ "documents": state["documents"],
203
+ }
204
+
205
+ graph = StateGraph(AgentState)
206
+ graph.add_node("extract", aextract)
207
+ graph.add_node("summarize", asummarize)
208
+
209
+ graph.add_edge(START, "extract")
210
+ graph.add_edge("extract", "summarize")
211
+ graph.add_edge("summarize", END)
212
+
213
+ return graph.compile()
@@ -0,0 +1,51 @@
1
+ import logging
2
+ from datetime import datetime
3
+ from typing import Sequence, Callable
4
+
5
+ from langchain_core.language_models import (
6
+ LanguageModelLike,
7
+ )
8
+ from langgraph.graph.graph import CompiledGraph
9
+ from langgraph.prebuilt import ToolNode, create_react_agent
10
+ from langchain_core.tools import BaseTool
11
+
12
+ from veri_agents_knowledgebase import Knowledgebase
13
+ from veri_agents_knowledgebase.tools import FixedKnowledgebaseWithTagsQuery, FixedKnowledgebaseListDocuments
14
+
15
+ log = logging.getLogger(__name__)
16
+
17
+ def create_qa_agent(
18
+ llm: LanguageModelLike,
19
+ knowledgebases: Sequence[Knowledgebase],
20
+ system_prompt: str,
21
+ tools: Sequence[BaseTool | Callable] | None = None,
22
+ **react_kwargs
23
+ ) -> CompiledGraph:
24
+ tools = list(tools) if tools else []
25
+ for i, knowledgebase in enumerate(knowledgebases):
26
+ tools.append(
27
+ FixedKnowledgebaseWithTagsQuery(
28
+ knowledgebase=knowledgebase,
29
+ num_results=10,
30
+ name_suffix=f"-{i}",
31
+ runnable_config_filter_prefix="filter_", # TODO: pick your own prefix for the runnable config
32
+ )
33
+ )
34
+ tools.append(
35
+ FixedKnowledgebaseListDocuments(
36
+ knowledgebase=knowledgebase,
37
+ name_suffix=f"-{i}",
38
+ runnable_config_filter_prefix="filter_", # TODO: pick your own prefix for the runnable config
39
+ )
40
+ )
41
+ tool_node = ToolNode(tools)
42
+
43
+ system_prompt = system_prompt
44
+ system_prompt += f"""Today's date is: {datetime.now().strftime("%Y-%m-%d")}."""
45
+
46
+ return create_react_agent(
47
+ model=llm,
48
+ tools=tool_node,
49
+ prompt=system_prompt,
50
+ **react_kwargs
51
+ )
@@ -0,0 +1,178 @@
1
+ import logging
2
+ from os import PathLike
3
+ from typing import Iterator, Optional, List, Dict, cast
4
+ from collections.abc import Sequence
5
+
6
+ from langchain_core.documents import Document
7
+ from pydantic import BaseModel, Field
8
+
9
+ log = logging.getLogger(__name__)
10
+
11
+
12
+ class DocumentLoader:
13
+ """Loads data from a data source and returns documents."""
14
+
15
+ def __init__(self):
16
+ pass
17
+
18
+ def load_documents(self, **kwargs):
19
+ """Parse documents from a data source."""
20
+ raise NotImplementedError
21
+
22
+
23
+ class KnowledgeFilter(BaseModel):
24
+ """Filter for knowledge base queries."""
25
+
26
+ docs: list[str] | str | None = None
27
+ """ List of document IDs or single document ID to filter by. """
28
+
29
+ tags_any_of: list[str] | str | None = None
30
+ """ List of tags to filter by, if any of the provided tags matches, a document is selected. """
31
+
32
+ tags_all_of: list[str] | str | None = None
33
+ """ List of tags to filter by, if all of the provided tags match, a document is selected. """
34
+
35
+ def __repr__(self):
36
+ return f"KnowledgeFilter(docs={self.docs}, tags_any_of={self.tags_any_of}, tags_all_of={self.tags_all_of})"
37
+
38
+ def __str__(self):
39
+ return f"KnowledgeFilter(docs={self.docs}, tags_any_of={self.tags_any_of}, tags_all_of={self.tags_all_of})"
40
+
41
+ def and_filters(filter1: KnowledgeFilter | None, filter2: KnowledgeFilter | None):
42
+ if filter1 is None:
43
+ return filter2
44
+ elif filter2 is None:
45
+ return filter1
46
+ else:
47
+ # docs
48
+ if filter1.docs is None:
49
+ docs = filter2.docs
50
+ elif filter2.docs is None:
51
+ docs = filter1.docs
52
+ else:
53
+ # intersection
54
+ docs1 = filter1.docs if isinstance(filter1.docs, Sequence) and not isinstance(filter1.docs, str) else [cast(str, filter1.docs)]
55
+ docs2 = filter2.docs if isinstance(filter2.docs, Sequence) and not isinstance(filter2.docs, str) else [cast(str, filter2.docs)]
56
+
57
+ docs = list(set(docs1) & set(docs2))
58
+
59
+ # tags_any_of
60
+ if filter1.tags_any_of is None:
61
+ tags_any_of = filter2.tags_any_of
62
+ elif filter2.tags_any_of is None:
63
+ tags_any_of = filter1.tags_any_of
64
+ else:
65
+ # union
66
+ tags_any_of1 = filter1.tags_any_of if isinstance(filter1.tags_any_of, Sequence) and not isinstance(filter1.tags_any_of, str) else [cast(str, filter1.tags_any_of)]
67
+ tags_any_of2 = filter2.tags_any_of if isinstance(filter2.tags_any_of, Sequence) and not isinstance(filter2.tags_any_of, str) else [cast(str, filter2.tags_any_of)]
68
+
69
+ tags_any_of = list(set(tags_any_of1) | set(tags_any_of2))
70
+
71
+ # tags_all_of
72
+ if filter1.tags_all_of is None:
73
+ tags_all_of = filter2.tags_all_of
74
+ elif filter2.tags_all_of is None:
75
+ tags_all_of = filter1.tags_all_of
76
+ else:
77
+ # union
78
+ tags_all_of1 = filter1.tags_all_of if isinstance(filter1.tags_all_of, Sequence) and not isinstance(filter1.tags_all_of, str) else [cast(str, filter1.tags_all_of)]
79
+ tags_all_of2 = filter2.tags_all_of if isinstance(filter2.tags_all_of, Sequence) and not isinstance(filter2.tags_all_of, str) else [cast(str, filter2.tags_all_of)]
80
+
81
+ tags_all_of = list(set(tags_all_of1) | set(tags_all_of2))
82
+
83
+ return KnowledgeFilter(
84
+ docs=docs,
85
+ tags_any_of=tags_any_of,
86
+ tags_all_of=tags_all_of
87
+ )
88
+
89
+
90
+ class DataSource(BaseModel):
91
+ """Data source for a knowledge base."""
92
+
93
+ location: PathLike | str = Field(
94
+ description="Location of the data source, e.g. a file path or URL."
95
+ )
96
+ name: str = Field(
97
+ description="Name of the data source. Can be used for filtering in the knowledgebase and important that document names are unique"
98
+ )
99
+ tags: list[str] = Field(
100
+ default=[],
101
+ description="Tags applied to all documents and chunks of the source, e.g. 'finance'.",
102
+ )
103
+ incremental: bool = Field(
104
+ default=False,
105
+ description="Whether to do incremental indexing of the data source.",
106
+ )
107
+
108
+ class KnowledgebaseMetadata(BaseModel):
109
+ """Metadata for a knowledgebase."""
110
+
111
+ name: str
112
+ description: str | None = None
113
+ tags: dict[str, str] = {}
114
+ collection: str
115
+ doc_summarize: bool = False
116
+ doc_autotag: bool = False
117
+ data_sources: List[Dict] = []
118
+
119
+ class Config:
120
+ extra = 'ignore'
121
+
122
+ class Knowledgebase:
123
+ def __init__(self, **kwargs):
124
+ print(f"Knowledgebase init: {kwargs}", flush=True)
125
+ self.metadata = KnowledgebaseMetadata.model_validate(kwargs)
126
+
127
+ @property
128
+ def tags(self):
129
+ """Get the tags for the workflow."""
130
+ return self.metadata.tags
131
+
132
+ @property
133
+ def name(self):
134
+ """Get the name of the workflow."""
135
+ return self.metadata.name
136
+
137
+ @property
138
+ def description(self):
139
+ """Get the description of the workflow."""
140
+ return self.metadata.description
141
+
142
+ def retrieve(
143
+ self,
144
+ query: str,
145
+ limit: int,
146
+ filter: KnowledgeFilter | None = None,
147
+ **kwargs,
148
+ ):
149
+ """Retrieve documents from the knowledge base."""
150
+ raise NotImplementedError
151
+
152
+ def get_documents(
153
+ self,
154
+ filter: KnowledgeFilter | None = None,
155
+ ) -> Iterator[Document]:
156
+ """Get all documents from the knowledge base."""
157
+ raise NotImplementedError
158
+
159
+
160
+ class RWKnowledgebase(Knowledgebase):
161
+ def __init__(self, **kwargs):
162
+ super().__init__(**kwargs)
163
+
164
+ def index(self, data_source: Optional[DataSource] = None):
165
+ """Do an index run on either a provides data source or data sources defined in its config.
166
+
167
+ Args:
168
+ data_source (DataSource): Data source to index. If None, will use the data sources defined in the config.
169
+ """
170
+ raise NotImplementedError
171
+
172
+ def set_tags(
173
+ self,
174
+ doc_id: str,
175
+ tags: list[str],
176
+ ):
177
+ """Add tags to a document."""
178
+ raise NotImplementedError
@@ -0,0 +1,5 @@
1
+ from .qdrant_doc_store import QdrantDocStore
2
+ from .qdrant_kb import QdrantKnowledgebase
3
+ from .generic_kb import GenericQdrantKnowledgebase
4
+ from .source_retriever import *
5
+ from .summarization import *