langroid 0.1.165__tar.gz → 0.1.167__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.
- {langroid-0.1.165 → langroid-0.1.167}/PKG-INFO +7 -1
- {langroid-0.1.165 → langroid-0.1.167}/README.md +4 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/chat_agent.py +2 -20
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/chat_document.py +10 -0
- langroid-0.1.167/langroid/agent/special/neo4j/neo4j_chat_agent.py +325 -0
- langroid-0.1.167/langroid/agent/special/neo4j/utils/system_message.py +39 -0
- langroid-0.1.167/langroid/agent/special/neo4j/utils/tools.py +13 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/tool_message.py +18 -1
- {langroid-0.1.165 → langroid-0.1.167}/langroid/language_models/base.py +4 -1
- {langroid-0.1.165 → langroid-0.1.167}/langroid/language_models/openai_gpt.py +1 -1
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/document_parser.py +26 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/parser.py +5 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/repo_loader.py +1 -1
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/url_loader.py +5 -1
- {langroid-0.1.165 → langroid-0.1.167}/pyproject.toml +3 -1
- {langroid-0.1.165 → langroid-0.1.167}/LICENSE +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/base.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/batch.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/helpers.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/junk +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/openai_assistant.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/doc_chat_agent.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/lance_doc_chat_agent.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/lance_rag/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/lance_rag/critic_agent.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/lance_rag/lance_rag_task.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/lance_rag/lance_tools.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/lance_rag/query_planner_agent.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/relevance_extractor_agent.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/retriever_agent.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/sql/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/sql/sql_chat_agent.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/sql/utils/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/sql/utils/description_extractors.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/sql/utils/populate_metadata.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/sql/utils/system_message.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/sql/utils/tools.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/table_chat_agent.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/task.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/tools/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/tools/extract_tool.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/tools/generator_tool.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/tools/google_search_tool.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/tools/recipient_tool.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/tools/run_python_code.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/tools/sciphi_search_rag_tool.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent/tools/segment_extract_tool.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/agent_config.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/cachedb/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/cachedb/base.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/cachedb/momento_cachedb.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/cachedb/redis_cachedb.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/embedding_models/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/embedding_models/base.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/embedding_models/clustering.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/embedding_models/models.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/language_models/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/language_models/azure_openai.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/language_models/config.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/language_models/openai_assistants.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/language_models/prompt_formatter/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/language_models/prompt_formatter/base.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/language_models/prompt_formatter/llama2_formatter.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/language_models/utils.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/mytypes.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/agent_chats.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/code-parsing.md +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/code_parser.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/config.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/json.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/para_sentence_split.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/search.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/spider.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/table_loader.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/url_loader_cookies.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/urls.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/utils.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/parsing/web_search.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/prompts/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/prompts/dialog.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/prompts/prompts_config.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/prompts/templates.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/prompts/transforms.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/algorithms/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/algorithms/graph.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/configuration.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/constants.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/docker.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/globals.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/llms/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/llms/strings.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/logging.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/output/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/output/printing.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/pandas_utils.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/pydantic_utils.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/system.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/web/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/web/login.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/utils/web/selenium_login.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/vector_store/__init__.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/vector_store/base.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/vector_store/chromadb.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/vector_store/lancedb.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/vector_store/meilisearch.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/vector_store/momento.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/vector_store/qdrant_cloud.py +0 -0
- {langroid-0.1.165 → langroid-0.1.167}/langroid/vector_store/qdrantdb.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: langroid
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.167
|
4
4
|
Summary: Harness LLMs with Multi-Agent Programming
|
5
5
|
License: MIT
|
6
6
|
Author: Prasad Chalasani
|
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Provides-Extra: hf-embeddings
|
14
14
|
Provides-Extra: litellm
|
15
15
|
Provides-Extra: mysql
|
16
|
+
Provides-Extra: neo4j
|
16
17
|
Provides-Extra: postgres
|
17
18
|
Provides-Extra: sciphi
|
18
19
|
Requires-Dist: agent-search (>=0.0.7,<0.0.8) ; extra == "sciphi"
|
@@ -48,6 +49,7 @@ Requires-Dist: mkdocs-section-index (>=0.3.5,<0.4.0)
|
|
48
49
|
Requires-Dist: mkdocstrings[python] (>=0.21.2,<0.22.0)
|
49
50
|
Requires-Dist: momento (>=1.10.2,<2.0.0)
|
50
51
|
Requires-Dist: mypy (>=1.7.0,<2.0.0)
|
52
|
+
Requires-Dist: neo4j (>=5.14.1,<6.0.0) ; extra == "neo4j"
|
51
53
|
Requires-Dist: nltk (>=3.8.1,<4.0.0)
|
52
54
|
Requires-Dist: onnxruntime (==1.16.1)
|
53
55
|
Requires-Dist: openai (>=1.2.3,<2.0.0)
|
@@ -138,6 +140,10 @@ This Multi-Agent paradigm is inspired by the
|
|
138
140
|
`Langroid` is a fresh take on LLM app-development, where considerable thought has gone
|
139
141
|
into simplifying the developer experience; it does not use `Langchain`.
|
140
142
|
|
143
|
+
:fire: See this [Intro to Langroid](https://lancedb.substack.com/p/langoid-multi-agent-programming-framework)
|
144
|
+
blog post from the LanceDB team
|
145
|
+
|
146
|
+
|
141
147
|
We welcome contributions -- See the [contributions](./CONTRIBUTING.md) document
|
142
148
|
for ideas on what to contribute.
|
143
149
|
|
@@ -45,6 +45,10 @@ This Multi-Agent paradigm is inspired by the
|
|
45
45
|
`Langroid` is a fresh take on LLM app-development, where considerable thought has gone
|
46
46
|
into simplifying the developer experience; it does not use `Langchain`.
|
47
47
|
|
48
|
+
:fire: See this [Intro to Langroid](https://lancedb.substack.com/p/langoid-multi-agent-programming-framework)
|
49
|
+
blog post from the LanceDB team
|
50
|
+
|
51
|
+
|
48
52
|
We welcome contributions -- See the [contributions](./CONTRIBUTING.md) document
|
49
53
|
for ideas on what to contribute.
|
50
54
|
|
@@ -207,26 +207,8 @@ class ChatAgent(Agent):
|
|
207
207
|
if msg_cls.default_value("request") in self.llm_tools_usable
|
208
208
|
]
|
209
209
|
)
|
210
|
-
return
|
211
|
-
|
212
|
-
=== ALL AVAILABLE TOOLS and THEIR JSON FORMAT INSTRUCTIONS ===
|
213
|
-
You have access to the following TOOLS to accomplish your task:
|
214
|
-
|
215
|
-
{json_instructions}
|
216
|
-
|
217
|
-
When one of the above TOOLs is applicable, you must express your
|
218
|
-
request as "TOOL:" followed by the request in the above JSON format.
|
219
|
-
"""
|
220
|
-
+ """
|
221
|
-
The JSON format will be:
|
222
|
-
\\{
|
223
|
-
"request": "<tool_name>",
|
224
|
-
"<arg1>": <value1>,
|
225
|
-
"<arg2>": <value2>,
|
226
|
-
...
|
227
|
-
\\}
|
228
|
-
----------------------------
|
229
|
-
""".lstrip()
|
210
|
+
return ToolMessage.json_group_instructions().format(
|
211
|
+
json_instructions=json_instructions
|
230
212
|
)
|
231
213
|
|
232
214
|
def tool_instructions(self) -> str:
|
@@ -192,6 +192,16 @@ class ChatDocument(Document):
|
|
192
192
|
if isinstance(message, ChatDocument):
|
193
193
|
content = message.content
|
194
194
|
fun_call = message.function_call
|
195
|
+
if message.metadata.sender == Entity.USER and fun_call is not None:
|
196
|
+
# This may happen when a (parent agent's) LLM generates a
|
197
|
+
# a Function-call, and it ends up being sent to the current task's
|
198
|
+
# LLM (possibly because the function-call is mis-named or has other
|
199
|
+
# issues and couldn't be handled by handler methods).
|
200
|
+
# But a function-call can only be generated by an entity with
|
201
|
+
# Role.ASSISTANT, so we instead put the content of the function-call
|
202
|
+
# in the content of the message.
|
203
|
+
content += " " + str(fun_call)
|
204
|
+
fun_call = None
|
195
205
|
sender_name = message.metadata.sender_name
|
196
206
|
tool_ids = message.metadata.tool_ids
|
197
207
|
tool_id = tool_ids[-1] if len(tool_ids) > 0 else ""
|
@@ -0,0 +1,325 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
3
|
+
|
4
|
+
from pydantic import BaseSettings
|
5
|
+
from rich import print
|
6
|
+
from rich.console import Console
|
7
|
+
|
8
|
+
if TYPE_CHECKING:
|
9
|
+
import neo4j
|
10
|
+
|
11
|
+
|
12
|
+
from langroid.agent.chat_agent import ChatAgent, ChatAgentConfig
|
13
|
+
from langroid.agent.chat_document import ChatDocMetaData, ChatDocument
|
14
|
+
from langroid.agent.special.neo4j.utils.system_message import (
|
15
|
+
DEFAULT_NEO4J_CHAT_SYSTEM_MESSAGE,
|
16
|
+
DEFAULT_SYS_MSG,
|
17
|
+
SCHEMA_TOOLS_SYS_MSG,
|
18
|
+
)
|
19
|
+
from langroid.agent.special.neo4j.utils.tools import (
|
20
|
+
GenerateCypherQueries,
|
21
|
+
GraphDatabaseSchema,
|
22
|
+
)
|
23
|
+
from langroid.mytypes import Entity
|
24
|
+
|
25
|
+
logger = logging.getLogger(__name__)
|
26
|
+
|
27
|
+
console = Console()
|
28
|
+
|
29
|
+
NEO4J_ERROR_MSG = "There was an error in your Cypher Query"
|
30
|
+
|
31
|
+
empty_nodes = "'nodes': []"
|
32
|
+
empty_relationships = "'relationships': []"
|
33
|
+
not_valid_query_response = [
|
34
|
+
empty_nodes,
|
35
|
+
empty_relationships,
|
36
|
+
NEO4J_ERROR_MSG,
|
37
|
+
]
|
38
|
+
|
39
|
+
|
40
|
+
class Neo4jSettings(BaseSettings):
|
41
|
+
uri: str = ""
|
42
|
+
username: str = ""
|
43
|
+
password: str = ""
|
44
|
+
database: str = ""
|
45
|
+
|
46
|
+
class Config:
|
47
|
+
# This enables the use of environment variables to set the settings,
|
48
|
+
# e.g. NEO4J_URI, NEO4J_USERNAME, etc.,
|
49
|
+
# which can either be set in a .env file or in the shell via export cmds.
|
50
|
+
env_prefix = "NEO4J_"
|
51
|
+
|
52
|
+
|
53
|
+
class Neo4jChatAgentConfig(ChatAgentConfig):
|
54
|
+
neo4j_settings: Neo4jSettings = Neo4jSettings()
|
55
|
+
system_message: str = DEFAULT_NEO4J_CHAT_SYSTEM_MESSAGE
|
56
|
+
kg_schema: Optional[List[Dict[str, Any]]]
|
57
|
+
database_created: bool = False
|
58
|
+
use_schema_tools: bool = False
|
59
|
+
use_functions_api: bool = True
|
60
|
+
use_tools: bool = False
|
61
|
+
|
62
|
+
|
63
|
+
class Neo4jChatAgent(ChatAgent):
|
64
|
+
def __init__(self, config: Neo4jChatAgentConfig):
|
65
|
+
"""Initialize the Neo4jChatAgent.
|
66
|
+
|
67
|
+
Raises:
|
68
|
+
ValueError: If database information is not provided in the config.
|
69
|
+
"""
|
70
|
+
self.config = config
|
71
|
+
self._validate_config()
|
72
|
+
self._import_neo4j()
|
73
|
+
self._initialize_connection()
|
74
|
+
self._init_tool_messages()
|
75
|
+
|
76
|
+
def _validate_config(self) -> None:
|
77
|
+
"""Validate the configuration to ensure all necessary fields are present."""
|
78
|
+
assert isinstance(self.config, Neo4jChatAgentConfig)
|
79
|
+
if (
|
80
|
+
self.config.neo4j_settings.username is None
|
81
|
+
and self.config.neo4j_settings.password is None
|
82
|
+
and self.config.neo4j_settings.database
|
83
|
+
):
|
84
|
+
raise ValueError("Neo4j env information must be provided")
|
85
|
+
|
86
|
+
def _import_neo4j(self) -> None:
|
87
|
+
"""Dynamically imports the Neo4j module and sets it as a global variable."""
|
88
|
+
global neo4j
|
89
|
+
try:
|
90
|
+
import neo4j
|
91
|
+
except ImportError:
|
92
|
+
raise ImportError(
|
93
|
+
"""
|
94
|
+
neo4j not installed. Please install it via:
|
95
|
+
pip install neo4j.
|
96
|
+
Or when installing langroid, install it with the `neo4j` extra:
|
97
|
+
pip install langroid[neo4j]
|
98
|
+
"""
|
99
|
+
)
|
100
|
+
|
101
|
+
def _initialize_connection(self) -> None:
|
102
|
+
"""
|
103
|
+
Initializes a connection to the Neo4j database using the configuration settings.
|
104
|
+
"""
|
105
|
+
try:
|
106
|
+
assert isinstance(self.config, Neo4jChatAgentConfig)
|
107
|
+
self.driver = neo4j.GraphDatabase.driver(
|
108
|
+
self.config.neo4j_settings.uri,
|
109
|
+
auth=(
|
110
|
+
self.config.neo4j_settings.username,
|
111
|
+
self.config.neo4j_settings.password,
|
112
|
+
),
|
113
|
+
)
|
114
|
+
except Exception as e:
|
115
|
+
raise ConnectionError(f"Failed to initialize Neo4j connection: {e}")
|
116
|
+
|
117
|
+
def close(self) -> None:
|
118
|
+
"""close the connection"""
|
119
|
+
if self.driver:
|
120
|
+
self.driver.close()
|
121
|
+
|
122
|
+
def retry_query(self, e: Exception, query: str) -> str:
|
123
|
+
"""
|
124
|
+
Generate an error message for a failed Cypher query and return it.
|
125
|
+
|
126
|
+
Args:
|
127
|
+
e (Exception): The exception raised during the Cypher query execution.
|
128
|
+
query (str): The Cypher query that failed.
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
str: The error message.
|
132
|
+
"""
|
133
|
+
logger.error(f"Cypher Query failed: {query}\nException: {e}")
|
134
|
+
|
135
|
+
# Construct the error message
|
136
|
+
error_message_template = f"""\
|
137
|
+
{NEO4J_ERROR_MSG}: '{query}'
|
138
|
+
{str(e)}
|
139
|
+
Run a new query, correcting the errors.
|
140
|
+
"""
|
141
|
+
|
142
|
+
return error_message_template
|
143
|
+
|
144
|
+
def read_query(
|
145
|
+
self, query: str, parameters: Optional[Dict[Any, Any]] = None
|
146
|
+
) -> str:
|
147
|
+
"""
|
148
|
+
Executes a given Cypher query with parameters on the Neo4j database.
|
149
|
+
|
150
|
+
Args:
|
151
|
+
query (str): The Cypher query string to be executed.
|
152
|
+
parameters (Optional[Dict[Any, Any]]): A dictionary of parameters for the
|
153
|
+
query. Defaults to None.
|
154
|
+
|
155
|
+
Returns:
|
156
|
+
str: The result of executing the Cypher query.
|
157
|
+
"""
|
158
|
+
response_message = ""
|
159
|
+
if not self.driver:
|
160
|
+
raise ValueError("No database connection is established.")
|
161
|
+
|
162
|
+
try:
|
163
|
+
assert isinstance(self.config, Neo4jChatAgentConfig)
|
164
|
+
with self.driver.session(
|
165
|
+
database=self.config.neo4j_settings.database
|
166
|
+
) as session:
|
167
|
+
result = session.run(query, parameters)
|
168
|
+
# Check if there are records in the result
|
169
|
+
if result.peek():
|
170
|
+
response_message = ", ".join(
|
171
|
+
[str(record.data()) for record in result]
|
172
|
+
)
|
173
|
+
else:
|
174
|
+
response_message = "No records found."
|
175
|
+
except Exception as e:
|
176
|
+
logger.error(f"Failed to execute query: {query}\n{e}")
|
177
|
+
response_message = self.retry_query(e, query)
|
178
|
+
finally:
|
179
|
+
self.close()
|
180
|
+
|
181
|
+
return response_message
|
182
|
+
|
183
|
+
def write_query(
|
184
|
+
self, query: str, parameters: Optional[Dict[Any, Any]] = None
|
185
|
+
) -> bool:
|
186
|
+
"""
|
187
|
+
Executes a write transaction using a given Cypher query on the Neo4j database.
|
188
|
+
This method should be used for queries that modify the database.
|
189
|
+
|
190
|
+
Args:
|
191
|
+
query (str): The Cypher query string to be executed.
|
192
|
+
parameters (dict, optional): A dict of parameters for the Cypher query
|
193
|
+
|
194
|
+
Returns:
|
195
|
+
bool: True if the query was executed successfully, False otherwise.
|
196
|
+
"""
|
197
|
+
if not self.driver:
|
198
|
+
raise ValueError("No database connection is established.")
|
199
|
+
response = False
|
200
|
+
try:
|
201
|
+
assert isinstance(self.config, Neo4jChatAgentConfig)
|
202
|
+
with self.driver.session(
|
203
|
+
database=self.config.neo4j_settings.database
|
204
|
+
) as session:
|
205
|
+
# Execute the query within a write transaction
|
206
|
+
session.write_transaction(lambda tx: tx.run(query, parameters))
|
207
|
+
response = True
|
208
|
+
except Exception as e:
|
209
|
+
logging.warning(
|
210
|
+
f"An unexpected error occurred while executing the write query: {e}"
|
211
|
+
)
|
212
|
+
finally:
|
213
|
+
self.close()
|
214
|
+
return response
|
215
|
+
|
216
|
+
# TODO: test under enterprise edition because community edition doesn't allow
|
217
|
+
# database creation/deletion
|
218
|
+
def remove_database(self) -> None:
|
219
|
+
"""Deletes all nodes and relationships from the current Neo4j database."""
|
220
|
+
delete_query = """
|
221
|
+
MATCH (n)
|
222
|
+
DETACH DELETE n
|
223
|
+
"""
|
224
|
+
if self.write_query(delete_query):
|
225
|
+
print("[green]Database is deleted!")
|
226
|
+
else:
|
227
|
+
print("[red]Database is not deleted!")
|
228
|
+
|
229
|
+
def make_query(self, msg: GenerateCypherQueries) -> str:
|
230
|
+
""" "
|
231
|
+
Handle a GenerateCypherQueries message by executing a Cypher query and
|
232
|
+
returning the result.
|
233
|
+
Args:
|
234
|
+
msg (GenerateCypherQueries): The tool-message to handle.
|
235
|
+
|
236
|
+
Returns:
|
237
|
+
str: The result of executing the Cypherquery.
|
238
|
+
"""
|
239
|
+
query = msg.cypherQuery
|
240
|
+
|
241
|
+
logger.info(f"Executing Cypher query: {query}")
|
242
|
+
return self.read_query(query)
|
243
|
+
|
244
|
+
# TODO: There are various ways to get the schema. The current one uses the func
|
245
|
+
# `read_query`, which requires post processing to identify whether the response upon
|
246
|
+
# the schema query is valid. Another way is to isolate this func from `read_query`.
|
247
|
+
# The current query works well. But we could use the queries here:
|
248
|
+
# https://github.com/neo4j/NaLLM/blob/1af09cd117ba0777d81075c597a5081583568f9f/api/
|
249
|
+
# src/driver/neo4j.py#L30
|
250
|
+
def get_schema(self, msg: GraphDatabaseSchema | None) -> str:
|
251
|
+
"""
|
252
|
+
Retrieves the schema of a Neo4j graph database.
|
253
|
+
|
254
|
+
Args:
|
255
|
+
msg (GraphDatabaseSchema): An instance of GraphDatabaseSchema, typically
|
256
|
+
containing information or parameters needed for the database query.
|
257
|
+
|
258
|
+
Returns:
|
259
|
+
str: The visual representation of the database schema as a string, or a
|
260
|
+
message stating that the database schema is empty or not valid.
|
261
|
+
|
262
|
+
Raises:
|
263
|
+
This function does not explicitly raise exceptions but depends on the
|
264
|
+
behavior of 'self.read_query' method, which might raise exceptions related
|
265
|
+
to database connectivity or query execution.
|
266
|
+
"""
|
267
|
+
schema = self.read_query("CALL db.schema.visualization()")
|
268
|
+
if not any(element in schema for element in not_valid_query_response):
|
269
|
+
return schema
|
270
|
+
else:
|
271
|
+
return "The database schema does not have any nodes or relationships."
|
272
|
+
|
273
|
+
def _init_tool_messages(self) -> None:
|
274
|
+
"""Initialize message tools used for chatting."""
|
275
|
+
message = self._format_message()
|
276
|
+
self.config.system_message = self.config.system_message.format(mode=message)
|
277
|
+
super().__init__(self.config)
|
278
|
+
self.enable_message(GenerateCypherQueries)
|
279
|
+
self.enable_message(GraphDatabaseSchema)
|
280
|
+
|
281
|
+
def _format_message(self) -> str:
|
282
|
+
if self.driver is None:
|
283
|
+
raise ValueError("Database driver None")
|
284
|
+
assert isinstance(self.config, Neo4jChatAgentConfig)
|
285
|
+
return (
|
286
|
+
SCHEMA_TOOLS_SYS_MSG.format(schema=self.get_schema(None))
|
287
|
+
if self.config.use_schema_tools
|
288
|
+
else DEFAULT_SYS_MSG
|
289
|
+
)
|
290
|
+
|
291
|
+
def agent_response(
|
292
|
+
self,
|
293
|
+
msg: Optional[str | ChatDocument] = None,
|
294
|
+
) -> Optional[ChatDocument]:
|
295
|
+
if msg is None:
|
296
|
+
return None
|
297
|
+
|
298
|
+
results = self.handle_message(msg)
|
299
|
+
if results is None:
|
300
|
+
return None
|
301
|
+
|
302
|
+
output = results
|
303
|
+
if NEO4J_ERROR_MSG in output:
|
304
|
+
output = "There was an error in the Cypher Query. Press enter to retry."
|
305
|
+
|
306
|
+
console.print(f"[red]{self.indent}", end="")
|
307
|
+
print(f"[red]Agent: {output}")
|
308
|
+
sender_name = self.config.name
|
309
|
+
if isinstance(msg, ChatDocument) and msg.function_call is not None:
|
310
|
+
sender_name = msg.function_call.name
|
311
|
+
|
312
|
+
content = results.content if isinstance(results, ChatDocument) else results
|
313
|
+
recipient = (
|
314
|
+
results.metadata.recipient if isinstance(results, ChatDocument) else ""
|
315
|
+
)
|
316
|
+
|
317
|
+
return ChatDocument(
|
318
|
+
content=content,
|
319
|
+
metadata=ChatDocMetaData(
|
320
|
+
# source=Entity.AGENT,
|
321
|
+
sender=Entity.LLM,
|
322
|
+
sender_name=sender_name,
|
323
|
+
recipient=recipient,
|
324
|
+
),
|
325
|
+
)
|
@@ -0,0 +1,39 @@
|
|
1
|
+
SCHEMA_TOOLS_SYS_MSG = """You are a data scientist and expert in Knolwedge Graphs,
|
2
|
+
with expertise in answering questions by interacting with a Neo4j graph database.
|
3
|
+
|
4
|
+
The schema maps the Neo4j database structure. node labels, relationship types,
|
5
|
+
and property keys available in your Neo4j database.
|
6
|
+
{schema}
|
7
|
+
Do not make assumptions about the database schema before using the tools.
|
8
|
+
Use the tool/function to learn more about the database schema."""
|
9
|
+
|
10
|
+
DEFAULT_SYS_MSG = """You are a data scientist and expert in Knolwedge Graphs,
|
11
|
+
with expertise in answering questions by querying Neo4j database.
|
12
|
+
You do not have access to the database directly, so you will need to use the
|
13
|
+
`make_query` tool/function-call to answer questions.
|
14
|
+
|
15
|
+
Use the `get_schema` tool/function-call to get all the node labels, relationship types,
|
16
|
+
and property keys available in your Neo4j database.
|
17
|
+
|
18
|
+
ONLY the node labels, relationship types, and property keys listed in the specified
|
19
|
+
above should be used in the generated queries.
|
20
|
+
You must be smart about using the right node labels, relationship types, and property
|
21
|
+
keys based on the english description. If you are thinking of using a node label,
|
22
|
+
relationship type, or property key that does not exist, you are probably on the wrong
|
23
|
+
track, so you should try your best to answer based on an existing table or column.
|
24
|
+
DO NOT assume any nodes or relationships other than those above."""
|
25
|
+
|
26
|
+
DEFAULT_NEO4J_CHAT_SYSTEM_MESSAGE = """
|
27
|
+
{mode}
|
28
|
+
|
29
|
+
You do not need to attempt answering a question with just one query.
|
30
|
+
You could make a sequence of Neo4j queries to help you write the final query.
|
31
|
+
Also if you receive a null or other unexpected result,
|
32
|
+
(a) make sure you use the available TOOLs correctly, and
|
33
|
+
(b) see if you have made an assumption in your Neo4j query, and try another way,
|
34
|
+
or use `run_query` to explore the database contents before submitting your
|
35
|
+
final query.
|
36
|
+
|
37
|
+
Start by asking what I would like to know about the data.
|
38
|
+
|
39
|
+
"""
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from langroid.agent.tool_message import ToolMessage
|
2
|
+
|
3
|
+
|
4
|
+
class GenerateCypherQueries(ToolMessage):
|
5
|
+
request: str = "make_query"
|
6
|
+
purpose: str = """Use this tool to send me the Generated Cypher query based on
|
7
|
+
text description and schema that I will provide you."""
|
8
|
+
cypherQuery: str
|
9
|
+
|
10
|
+
|
11
|
+
class GraphDatabaseSchema(ToolMessage):
|
12
|
+
request: str = "get_schema"
|
13
|
+
purpose: str = """Use this tool to get me the schema of the graph database."""
|
@@ -103,7 +103,7 @@ class ToolMessage(ABC, BaseModel):
|
|
103
103
|
@classmethod
|
104
104
|
def json_instructions(cls) -> str:
|
105
105
|
"""
|
106
|
-
Default Instructions to the LLM showing how to use the
|
106
|
+
Default Instructions to the LLM showing how to use the tool/function-call.
|
107
107
|
Works for GPT4 but override this for weaker LLMs if needed.
|
108
108
|
Returns:
|
109
109
|
str: instructions on how to use the message
|
@@ -122,6 +122,23 @@ class ToolMessage(ABC, BaseModel):
|
|
122
122
|
""".lstrip()
|
123
123
|
)
|
124
124
|
|
125
|
+
@staticmethod
|
126
|
+
def json_group_instructions() -> str:
|
127
|
+
"""Template for instructions for a group of tools.
|
128
|
+
Works with GPT4 but override this for weaker LLMs if needed.
|
129
|
+
"""
|
130
|
+
return textwrap.dedent(
|
131
|
+
"""
|
132
|
+
=== ALL AVAILABLE TOOLS and THEIR JSON FORMAT INSTRUCTIONS ===
|
133
|
+
You have access to the following TOOLS to accomplish your task:
|
134
|
+
|
135
|
+
{json_instructions}
|
136
|
+
|
137
|
+
When one of the above TOOLs is applicable, you must express your
|
138
|
+
request as "TOOL:" followed by the request in the above JSON format.
|
139
|
+
"""
|
140
|
+
)
|
141
|
+
|
125
142
|
@classmethod
|
126
143
|
def llm_function_schema(
|
127
144
|
cls,
|
@@ -3,11 +3,12 @@ import asyncio
|
|
3
3
|
import json
|
4
4
|
import logging
|
5
5
|
from abc import ABC, abstractmethod
|
6
|
+
from datetime import datetime
|
6
7
|
from enum import Enum
|
7
8
|
from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
8
9
|
|
9
10
|
import aiohttp
|
10
|
-
from pydantic import BaseModel, BaseSettings
|
11
|
+
from pydantic import BaseModel, BaseSettings, Field
|
11
12
|
|
12
13
|
from langroid.cachedb.momento_cachedb import MomentoCacheConfig
|
13
14
|
from langroid.cachedb.redis_cachedb import RedisCacheConfig
|
@@ -135,6 +136,7 @@ class LLMMessage(BaseModel):
|
|
135
136
|
tool_id: str = "" # used by OpenAIAssistant
|
136
137
|
content: str
|
137
138
|
function_call: Optional[LLMFunctionCall] = None
|
139
|
+
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
138
140
|
|
139
141
|
def api_dict(self) -> Dict[str, Any]:
|
140
142
|
"""
|
@@ -157,6 +159,7 @@ class LLMMessage(BaseModel):
|
|
157
159
|
dict_no_none["function_call"]["arguments"]
|
158
160
|
)
|
159
161
|
dict_no_none.pop("tool_id", None)
|
162
|
+
dict_no_none.pop("timestamp", None)
|
160
163
|
return dict_no_none
|
161
164
|
|
162
165
|
def __str__(self) -> str:
|
@@ -931,7 +931,7 @@ class OpenAIGPT(LanguageModel):
|
|
931
931
|
self._cache_store(hashed_key, result.model_dump())
|
932
932
|
return cached, hashed_key, result
|
933
933
|
|
934
|
-
@
|
934
|
+
@async_retry_with_exponential_backoff
|
935
935
|
async def _achat_completions_with_backoff(self, **kwargs): # type: ignore
|
936
936
|
cached = False
|
937
937
|
hashed_key, result = self._cache_lookup("Completion", **kwargs)
|
@@ -19,6 +19,7 @@ logger = logging.getLogger(__name__)
|
|
19
19
|
class DocumentType(str, Enum):
|
20
20
|
PDF = "pdf"
|
21
21
|
DOCX = "docx"
|
22
|
+
DOC = "doc"
|
22
23
|
|
23
24
|
|
24
25
|
class DocumentParser(Parser):
|
@@ -68,6 +69,8 @@ class DocumentParser(Parser):
|
|
68
69
|
raise ValueError(
|
69
70
|
f"Unsupported DOCX library specified: {config.docx.library}"
|
70
71
|
)
|
72
|
+
elif DocumentParser._document_type(source) == DocumentType.DOC:
|
73
|
+
return UnstructuredDocParser(source, config)
|
71
74
|
else:
|
72
75
|
raise ValueError(f"Unsupported document type: {source}")
|
73
76
|
|
@@ -98,6 +101,8 @@ class DocumentParser(Parser):
|
|
98
101
|
return DocumentType.PDF
|
99
102
|
elif source.lower().endswith(".docx"):
|
100
103
|
return DocumentType.DOCX
|
104
|
+
elif source.lower().endswith(".doc"):
|
105
|
+
return DocumentType.DOC
|
101
106
|
else:
|
102
107
|
raise ValueError(f"Unsupported document type: {source}")
|
103
108
|
|
@@ -440,6 +445,27 @@ class UnstructuredDocxParser(DocumentParser):
|
|
440
445
|
return self.fix_text(text)
|
441
446
|
|
442
447
|
|
448
|
+
class UnstructuredDocParser(UnstructuredDocxParser):
|
449
|
+
def iterate_pages(self) -> Generator[Tuple[int, Any], None, None]: # type: ignore
|
450
|
+
from unstructured.partition.doc import partition_doc
|
451
|
+
|
452
|
+
elements = partition_doc(filename=self.source, include_page_breaks=True)
|
453
|
+
|
454
|
+
page_number = 1
|
455
|
+
page_elements = [] # type: ignore
|
456
|
+
for el in elements:
|
457
|
+
if el.category == "PageBreak":
|
458
|
+
if page_elements: # Avoid yielding empty pages at the start
|
459
|
+
yield page_number, page_elements
|
460
|
+
page_number += 1
|
461
|
+
page_elements = []
|
462
|
+
else:
|
463
|
+
page_elements.append(el)
|
464
|
+
# Yield the last page if it's not empty
|
465
|
+
if page_elements:
|
466
|
+
yield page_number, page_elements
|
467
|
+
|
468
|
+
|
443
469
|
class PythonDocxParser(DocumentParser):
|
444
470
|
"""
|
445
471
|
Parser for processing DOCX files using the `python-docx` library.
|
@@ -28,6 +28,10 @@ class DocxParsingConfig(BaseSettings):
|
|
28
28
|
library: Literal["python-docx", "unstructured"] = "unstructured"
|
29
29
|
|
30
30
|
|
31
|
+
class DocParsingConfig(BaseSettings):
|
32
|
+
library: Literal["unstructured"] = "unstructured"
|
33
|
+
|
34
|
+
|
31
35
|
class ParsingConfig(BaseSettings):
|
32
36
|
splitter: str = Splitter.TOKENS
|
33
37
|
chunk_size: int = 200 # aim for this many tokens per chunk
|
@@ -42,6 +46,7 @@ class ParsingConfig(BaseSettings):
|
|
42
46
|
token_encoding_model: str = "text-embedding-ada-002"
|
43
47
|
pdf: PdfParsingConfig = PdfParsingConfig()
|
44
48
|
docx: DocxParsingConfig = DocxParsingConfig()
|
49
|
+
doc: DocParsingConfig = DocParsingConfig()
|
45
50
|
|
46
51
|
|
47
52
|
class Parser:
|
@@ -545,7 +545,7 @@ class RepoLoader:
|
|
545
545
|
|
546
546
|
for file_path in file_paths:
|
547
547
|
_, file_extension = os.path.splitext(file_path)
|
548
|
-
if file_extension.lower() in [".pdf", ".docx"]:
|
548
|
+
if file_extension.lower() in [".pdf", ".docx", ".doc"]:
|
549
549
|
doc_parser = DocumentParser.create(
|
550
550
|
file_path,
|
551
551
|
parser.config,
|
@@ -44,7 +44,11 @@ class URLLoader:
|
|
44
44
|
sleep_time=5,
|
45
45
|
)
|
46
46
|
for url, result in buffered_downloads(buffer, threads):
|
47
|
-
if
|
47
|
+
if (
|
48
|
+
url.lower().endswith(".pdf")
|
49
|
+
or url.lower().endswith(".docx")
|
50
|
+
or url.lower().endswith(".doc")
|
51
|
+
):
|
48
52
|
doc_parser = DocumentParser.create(
|
49
53
|
url,
|
50
54
|
self.parser.config,
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "langroid"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.167"
|
4
4
|
description = "Harness LLMs with Multi-Agent Programming"
|
5
5
|
authors = ["Prasad Chalasani <pchalasani@gmail.com>"]
|
6
6
|
readme = "README.md"
|
@@ -81,6 +81,7 @@ scrapy = "^2.11.0"
|
|
81
81
|
async-generator = "^1.10"
|
82
82
|
lancedb = "^0.4.1"
|
83
83
|
pytest-redis = "^3.0.2"
|
84
|
+
neo4j = {version = "^5.14.1", optional = true}
|
84
85
|
agent-search = {version = "^0.0.7", optional = true}
|
85
86
|
python-docx = "^1.1.0"
|
86
87
|
aiohttp = "^3.9.1"
|
@@ -91,6 +92,7 @@ hf-embeddings = ["sentence-transformers", "torch"]
|
|
91
92
|
postgres = ["psycopg2", "pytest-postgresql"]
|
92
93
|
mysql = ["pymysql", "pytest-mysql"]
|
93
94
|
litellm = ["litellm"]
|
95
|
+
neo4j = ["neo4j"]
|
94
96
|
sciphi = ["agent-search"]
|
95
97
|
|
96
98
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/lance_rag/query_planner_agent.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{langroid-0.1.165 → langroid-0.1.167}/langroid/agent/special/sql/utils/description_extractors.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{langroid-0.1.165 → langroid-0.1.167}/langroid/language_models/prompt_formatter/llama2_formatter.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|