veri-agents-knowledgebase 0.1.1__tar.gz

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.
Files changed (19) hide show
  1. veri_agents_knowledgebase-0.1.1/.gitignore +186 -0
  2. veri_agents_knowledgebase-0.1.1/PKG-INFO +17 -0
  3. veri_agents_knowledgebase-0.1.1/package.json +16 -0
  4. veri_agents_knowledgebase-0.1.1/pyproject.toml +27 -0
  5. veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/__init__.py +1 -0
  6. veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/graphs/__init__.py +2 -0
  7. veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/graphs/extract.py +213 -0
  8. veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/graphs/qa.py +51 -0
  9. veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/knowledgebase.py +178 -0
  10. veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/qdrant/__init__.py +5 -0
  11. veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/qdrant/generic_kb.py +261 -0
  12. veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/qdrant/qdrant_doc_store.py +153 -0
  13. veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/qdrant/qdrant_kb.py +119 -0
  14. veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/qdrant/source_retriever.py +113 -0
  15. veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/qdrant/summarization.py +64 -0
  16. veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/qdrant/tests/qdrant_doc_store_test.py +39 -0
  17. veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/tools/__init__.py +1 -0
  18. veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/tools/knowledge_retrieval.py +252 -0
  19. veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/utils.py +217 -0
@@ -0,0 +1,186 @@
1
+ .turbo
2
+
3
+ # go
4
+ vendor
5
+
6
+ # js
7
+ dist
8
+ out-tsc
9
+ node_modules
10
+
11
+ .idea
12
+ *.iml
13
+ .DS_Store
14
+
15
+
16
+ # Byte-compiled / optimized / DLL files
17
+ __pycache__/
18
+ *.py[cod]
19
+ *$py.class
20
+
21
+ # C extensions
22
+ *.so
23
+
24
+ # Distribution / packaging
25
+ .Python
26
+ build/
27
+ develop-eggs/
28
+ dist/
29
+ downloads/
30
+ eggs/
31
+ .eggs/
32
+ lib/
33
+ lib64/
34
+ parts/
35
+ sdist/
36
+ var/
37
+ wheels/
38
+ share/python-wheels/
39
+ *.egg-info/
40
+ .installed.cfg
41
+ *.egg
42
+ MANIFEST
43
+
44
+ # PyInstaller
45
+ # Usually these files are written by a python script from a template
46
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
47
+ *.manifest
48
+ *.spec
49
+
50
+ # Installer logs
51
+ pip-log.txt
52
+ pip-delete-this-directory.txt
53
+
54
+ # Unit test / coverage reports
55
+ htmlcov/
56
+ .tox/
57
+ .nox/
58
+ .coverage
59
+ .coverage.*
60
+ .cache
61
+ nosetests.xml
62
+ coverage.xml
63
+ *.cover
64
+ *.py,cover
65
+ .hypothesis/
66
+ .pytest_cache/
67
+ cover/
68
+
69
+ # Translations
70
+ *.mo
71
+ *.pot
72
+
73
+ # Django stuff:
74
+ *.log
75
+ local_settings.py
76
+ db.sqlite3
77
+ db.sqlite3-journal
78
+
79
+ # Flask stuff:
80
+ instance/
81
+ .webassets-cache
82
+
83
+ # Scrapy stuff:
84
+ .scrapy
85
+
86
+ # Sphinx documentation
87
+ docs/_build/
88
+
89
+ # PyBuilder
90
+ .pybuilder/
91
+ target/
92
+
93
+ # Jupyter Notebook
94
+ .ipynb_checkpoints
95
+
96
+ # IPython
97
+ profile_default/
98
+ ipython_config.py
99
+
100
+ # pyenv
101
+ # For a library or package, you might want to ignore these files since the code is
102
+ # intended to run in multiple environments; otherwise, check them in:
103
+ # .python-version
104
+
105
+ # pipenv
106
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
107
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
108
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
109
+ # install all needed dependencies.
110
+ #Pipfile.lock
111
+
112
+ # UV
113
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
114
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
115
+ # commonly ignored for libraries.
116
+ #uv.lock
117
+
118
+ # poetry
119
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
120
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
121
+ # commonly ignored for libraries.
122
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
123
+ #poetry.lock
124
+
125
+ # pdm
126
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
127
+ #pdm.lock
128
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
129
+ # in version control.
130
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
131
+ .pdm.toml
132
+ .pdm-python
133
+ .pdm-build/
134
+
135
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
136
+ __pypackages__/
137
+
138
+ # Celery stuff
139
+ celerybeat-schedule
140
+ celerybeat.pid
141
+
142
+ # SageMath parsed files
143
+ *.sage.py
144
+
145
+ # Environments
146
+ .env
147
+ .venv
148
+ env/
149
+ venv/
150
+ ENV/
151
+ env.bak/
152
+ venv.bak/
153
+
154
+ # Spyder project settings
155
+ .spyderproject
156
+ .spyproject
157
+
158
+ # Rope project settings
159
+ .ropeproject
160
+
161
+ # mkdocs documentation
162
+ /site
163
+
164
+ # mypy
165
+ .mypy_cache/
166
+ .dmypy.json
167
+ dmypy.json
168
+
169
+ # Pyre type checker
170
+ .pyre/
171
+
172
+ # pytype static type analyzer
173
+ .pytype/
174
+
175
+ # Cython debug symbols
176
+ cython_debug/
177
+
178
+ # PyCharm
179
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
180
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
181
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
182
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
183
+ #.idea/
184
+
185
+ # PyPI configuration file
186
+ .pypirc
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: veri-agents-knowledgebase
3
+ Version: 0.1.1
4
+ Summary: Add your description here
5
+ Author-email: Markus Toman <mtoman@veritone.com>, Teo Boley <tboley@veritone.com>
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: veri-agents-common==0.1.1
8
+ Provides-Extra: all
9
+ Requires-Dist: veri-agents-common[docling,fastembed,qdrant]==0.1.1; extra == 'all'
10
+ Requires-Dist: veri-agents-common[langgraph]==0.1.1; extra == 'all'
11
+ Provides-Extra: dev
12
+ Requires-Dist: veri-agents-common[docling,fastembed,qdrant]==0.1.1; extra == 'dev'
13
+ Requires-Dist: veri-agents-common[langgraph]==0.1.1; extra == 'dev'
14
+ Provides-Extra: graphs
15
+ Requires-Dist: veri-agents-common[langgraph]==0.1.1; extra == 'graphs'
16
+ Provides-Extra: qdrant
17
+ Requires-Dist: veri-agents-common[docling,fastembed,qdrant]==0.1.1; extra == 'qdrant'
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@veritone/agents-knowledgebase",
3
+ "version": "0.0.0",
4
+ "description": "",
5
+ "type": "module",
6
+ "scripts": {
7
+ "typecheck": "uv run pyright",
8
+ "format": "uv run ruff format",
9
+ "lint": "echo \"Warn: no lint specified\" && exit 0",
10
+ "test": "echo \"Warn: no test specified\" && exit 0"
11
+ },
12
+ "author": "",
13
+ "dependencies": {
14
+ "@veritone/agents-common": "workspace:"
15
+ }
16
+ }
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "veri-agents-knowledgebase"
3
+ version = "0.1.1"
4
+ description = "Add your description here"
5
+ authors = [
6
+ {name = "Markus Toman", email = "mtoman@veritone.com"},
7
+ {name = "Teo Boley", email = "tboley@veritone.com"},
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "veri-agents-common==0.1.1"
12
+ ]
13
+
14
+ [project.optional-dependencies]
15
+ graphs = [
16
+ "veri-agents-common[langgraph]==0.1.1",
17
+ ]
18
+ qdrant = [
19
+ "veri-agents-common[qdrant,docling,fastembed]==0.1.1",
20
+ ]
21
+ all = ["veri-agents-knowledgebase[graphs,qdrant]==0.1.1"]
22
+ # as optional dep so it can be referenced in workspace pyproject.toml
23
+ dev = ["veri-agents-knowledgebase[all]==0.1.1"]
24
+
25
+ [build-system]
26
+ requires = ["hatchling"]
27
+ build-backend = "hatchling.build"
@@ -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