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.
- veri_agents_knowledgebase-0.1.1/.gitignore +186 -0
- veri_agents_knowledgebase-0.1.1/PKG-INFO +17 -0
- veri_agents_knowledgebase-0.1.1/package.json +16 -0
- veri_agents_knowledgebase-0.1.1/pyproject.toml +27 -0
- veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/__init__.py +1 -0
- veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/graphs/__init__.py +2 -0
- veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/graphs/extract.py +213 -0
- veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/graphs/qa.py +51 -0
- veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/knowledgebase.py +178 -0
- veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/qdrant/__init__.py +5 -0
- veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/qdrant/generic_kb.py +261 -0
- veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/qdrant/qdrant_doc_store.py +153 -0
- veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/qdrant/qdrant_kb.py +119 -0
- veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/qdrant/source_retriever.py +113 -0
- veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/qdrant/summarization.py +64 -0
- veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/qdrant/tests/qdrant_doc_store_test.py +39 -0
- veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/tools/__init__.py +1 -0
- veri_agents_knowledgebase-0.1.1/src/veri_agents_knowledgebase/tools/knowledge_retrieval.py +252 -0
- 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,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
|