ws-bom-robot-app 0.0.63__py3-none-any.whl → 0.0.103__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.
- ws_bom_robot_app/config.py +30 -8
- ws_bom_robot_app/cron_manager.py +13 -12
- ws_bom_robot_app/llm/agent_context.py +1 -1
- ws_bom_robot_app/llm/agent_handler.py +11 -12
- ws_bom_robot_app/llm/agent_lcel.py +80 -18
- ws_bom_robot_app/llm/api.py +69 -7
- ws_bom_robot_app/llm/evaluator.py +319 -0
- ws_bom_robot_app/llm/main.py +51 -28
- ws_bom_robot_app/llm/models/api.py +40 -6
- ws_bom_robot_app/llm/nebuly_handler.py +18 -15
- ws_bom_robot_app/llm/providers/llm_manager.py +233 -75
- ws_bom_robot_app/llm/tools/tool_builder.py +4 -1
- ws_bom_robot_app/llm/tools/tool_manager.py +48 -22
- ws_bom_robot_app/llm/utils/chunker.py +6 -1
- ws_bom_robot_app/llm/utils/cleanup.py +81 -0
- ws_bom_robot_app/llm/utils/cms.py +60 -14
- ws_bom_robot_app/llm/utils/download.py +112 -8
- ws_bom_robot_app/llm/vector_store/db/base.py +50 -0
- ws_bom_robot_app/llm/vector_store/db/chroma.py +28 -8
- ws_bom_robot_app/llm/vector_store/db/faiss.py +35 -8
- ws_bom_robot_app/llm/vector_store/db/qdrant.py +29 -14
- ws_bom_robot_app/llm/vector_store/integration/api.py +216 -0
- ws_bom_robot_app/llm/vector_store/integration/azure.py +1 -1
- ws_bom_robot_app/llm/vector_store/integration/base.py +58 -15
- ws_bom_robot_app/llm/vector_store/integration/confluence.py +33 -5
- ws_bom_robot_app/llm/vector_store/integration/dropbox.py +1 -1
- ws_bom_robot_app/llm/vector_store/integration/gcs.py +1 -1
- ws_bom_robot_app/llm/vector_store/integration/github.py +22 -22
- ws_bom_robot_app/llm/vector_store/integration/googledrive.py +46 -17
- ws_bom_robot_app/llm/vector_store/integration/jira.py +93 -60
- ws_bom_robot_app/llm/vector_store/integration/manager.py +6 -2
- ws_bom_robot_app/llm/vector_store/integration/s3.py +1 -1
- ws_bom_robot_app/llm/vector_store/integration/sftp.py +1 -1
- ws_bom_robot_app/llm/vector_store/integration/sharepoint.py +7 -14
- ws_bom_robot_app/llm/vector_store/integration/shopify.py +143 -0
- ws_bom_robot_app/llm/vector_store/integration/sitemap.py +6 -1
- ws_bom_robot_app/llm/vector_store/integration/slack.py +3 -2
- ws_bom_robot_app/llm/vector_store/integration/thron.py +236 -0
- ws_bom_robot_app/llm/vector_store/loader/base.py +52 -8
- ws_bom_robot_app/llm/vector_store/loader/docling.py +71 -33
- ws_bom_robot_app/main.py +148 -146
- ws_bom_robot_app/subprocess_runner.py +106 -0
- ws_bom_robot_app/task_manager.py +204 -53
- ws_bom_robot_app/util.py +6 -0
- {ws_bom_robot_app-0.0.63.dist-info → ws_bom_robot_app-0.0.103.dist-info}/METADATA +158 -75
- ws_bom_robot_app-0.0.103.dist-info/RECORD +76 -0
- ws_bom_robot_app/llm/settings.py +0 -4
- ws_bom_robot_app/llm/utils/kb.py +0 -34
- ws_bom_robot_app-0.0.63.dist-info/RECORD +0 -72
- {ws_bom_robot_app-0.0.63.dist-info → ws_bom_robot_app-0.0.103.dist-info}/WHEEL +0 -0
- {ws_bom_robot_app-0.0.63.dist-info → ws_bom_robot_app-0.0.103.dist-info}/top_level.txt +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import json
|
|
2
1
|
from typing import Optional
|
|
2
|
+
from urllib.parse import urlparse
|
|
3
3
|
from langchain_core.embeddings import Embeddings
|
|
4
4
|
from langchain_core.language_models import BaseChatModel
|
|
5
5
|
from pydantic import BaseModel, ConfigDict, Field
|
|
6
6
|
import os
|
|
7
|
+
from ws_bom_robot_app.llm.utils.download import Base64File
|
|
7
8
|
|
|
8
9
|
class LlmConfig(BaseModel):
|
|
9
10
|
api_url: Optional[str] = None
|
|
@@ -36,6 +37,76 @@ class LlmInterface:
|
|
|
36
37
|
def get_parser(self):
|
|
37
38
|
from langchain.agents.output_parsers.openai_tools import OpenAIToolsAgentOutputParser
|
|
38
39
|
return OpenAIToolsAgentOutputParser()
|
|
40
|
+
async def _format_multimodal_image_message(self, message: dict) -> dict:
|
|
41
|
+
return {
|
|
42
|
+
"type": "image_url",
|
|
43
|
+
"image_url": {
|
|
44
|
+
"url": message.get("url")
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async def _format_multimodal_file_message(self, message: dict, file: Base64File = None) -> dict:
|
|
48
|
+
_file = file or await Base64File.from_url(message.get("url"))
|
|
49
|
+
return {"type": "text", "text": f"Here's a file attachment named `{_file.name}` of type `{_file.mime_type}` in base64: `{_file.base64_content}`"}
|
|
50
|
+
async def format_multimodal_content(self, content: list) -> list:
|
|
51
|
+
_content = []
|
|
52
|
+
for message in content:
|
|
53
|
+
if isinstance(message, dict):
|
|
54
|
+
if message.get("type") == "image" and "url" in message:
|
|
55
|
+
_content.append(await self._format_multimodal_image_message(message))
|
|
56
|
+
elif message.get("type") == "file" and "url" in message:
|
|
57
|
+
_content.append(await self._format_multimodal_file_message(message))
|
|
58
|
+
else:
|
|
59
|
+
# pass through text or other formats unchanged
|
|
60
|
+
_content.append(message)
|
|
61
|
+
else:
|
|
62
|
+
_content.append(message)
|
|
63
|
+
return _content
|
|
64
|
+
|
|
65
|
+
class Anthropic(LlmInterface):
|
|
66
|
+
def get_llm(self):
|
|
67
|
+
from langchain_anthropic import ChatAnthropic
|
|
68
|
+
return ChatAnthropic(
|
|
69
|
+
api_key=self.config.api_key or os.getenv("ANTHROPIC_API_KEY"),
|
|
70
|
+
model=self.config.model,
|
|
71
|
+
temperature=self.config.temperature,
|
|
72
|
+
max_tokens=8192,
|
|
73
|
+
streaming=True,
|
|
74
|
+
#betas=["files-api-2025-04-14"] #https://docs.anthropic.com/en/docs/build-with-claude/files
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
def get_embeddings(self):
|
|
79
|
+
from langchain_voyageai import VoyageAIEmbeddings
|
|
80
|
+
return VoyageAIEmbeddings(
|
|
81
|
+
api_key=self.config.embedding_api_key, #voyage api key
|
|
82
|
+
model="voyage-3")
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def get_models(self):
|
|
86
|
+
import anthropic
|
|
87
|
+
client = anthropic.Client(api_key=self.config.api_key or os.getenv("ANTHROPIC_API_KEY"))
|
|
88
|
+
response = client.models.list()
|
|
89
|
+
return response.data
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
async def _format_multimodal_image_message(self, message: dict) -> dict:
|
|
93
|
+
file = await Base64File.from_url(message.get("url"))
|
|
94
|
+
return { "type": "image_url", "image_url": { "url": file.base64_url }}
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
#https://python.langchain.com/docs/integrations/chat/anthropic/
|
|
98
|
+
#https://python.langchain.com/docs/how_to/multimodal_inputs/
|
|
99
|
+
async def _format_multimodal_file_message(self, message: dict, file: Base64File = None) -> dict:
|
|
100
|
+
_url = str(message.get("url", ""))
|
|
101
|
+
_url_lower = _url.lower()
|
|
102
|
+
if _url_lower.startswith("http") and any(urlparse(_url_lower).path.endswith(ext) for ext in [".pdf"]):
|
|
103
|
+
return {"type": "file", "source_type": "url", "url": _url}
|
|
104
|
+
else:
|
|
105
|
+
_file = file or await Base64File.from_url(_url)
|
|
106
|
+
if _file.extension in ["pdf"]:
|
|
107
|
+
return {"type": "document", "source": {"type": "base64", "media_type": _file.mime_type, "data": _file.base64_content}}
|
|
108
|
+
else:
|
|
109
|
+
return await super()._format_multimodal_file_message(message, _file)
|
|
39
110
|
|
|
40
111
|
class OpenAI(LlmInterface):
|
|
41
112
|
def __init__(self, config: LlmConfig):
|
|
@@ -47,8 +118,9 @@ class OpenAI(LlmInterface):
|
|
|
47
118
|
chat = ChatOpenAI(
|
|
48
119
|
api_key=self.config.api_key or os.getenv("OPENAI_API_KEY"),
|
|
49
120
|
model=self.config.model,
|
|
50
|
-
|
|
51
|
-
|
|
121
|
+
streaming=True
|
|
122
|
+
)
|
|
123
|
+
if not (any(self.config.model.startswith(prefix) for prefix in ["gpt-5", "o1", "o3"]) or "search" in self.config.model):
|
|
52
124
|
chat.temperature = self.config.temperature
|
|
53
125
|
chat.streaming = True
|
|
54
126
|
return chat
|
|
@@ -59,6 +131,13 @@ class OpenAI(LlmInterface):
|
|
|
59
131
|
response = openai.models.list()
|
|
60
132
|
return response.data
|
|
61
133
|
|
|
134
|
+
async def _format_multimodal_file_message(self, message: dict, file: Base64File = None) -> dict:
|
|
135
|
+
_file = file or await Base64File.from_url(message.get("url"))
|
|
136
|
+
if _file.extension in ["pdf"]:
|
|
137
|
+
return {"type": "file", "file": { "source_type": "base64", "file_data": _file.base64_url, "mime_type": _file.mime_type, "filename": _file.name}}
|
|
138
|
+
else:
|
|
139
|
+
return await super()._format_multimodal_file_message(message, _file)
|
|
140
|
+
|
|
62
141
|
class DeepSeek(LlmInterface):
|
|
63
142
|
def get_llm(self):
|
|
64
143
|
from langchain_openai import ChatOpenAI
|
|
@@ -68,8 +147,7 @@ class DeepSeek(LlmInterface):
|
|
|
68
147
|
base_url="https://api.deepseek.com",
|
|
69
148
|
max_tokens=8192,
|
|
70
149
|
temperature=self.config.temperature,
|
|
71
|
-
streaming=True
|
|
72
|
-
stream_usage=True,
|
|
150
|
+
streaming=True
|
|
73
151
|
)
|
|
74
152
|
|
|
75
153
|
def get_models(self):
|
|
@@ -79,36 +157,50 @@ class DeepSeek(LlmInterface):
|
|
|
79
157
|
response = openai.models.list()
|
|
80
158
|
return response.data
|
|
81
159
|
|
|
160
|
+
async def _format_multimodal_image_message(self, message: dict) -> dict:
|
|
161
|
+
print(f"{DeepSeek.__name__} does not support image messages")
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
async def _format_multimodal_file_message(self, message: dict, file: Base64File = None) -> dict:
|
|
165
|
+
print(f"{DeepSeek.__name__} does not support file messages")
|
|
166
|
+
return None
|
|
167
|
+
|
|
82
168
|
class Google(LlmInterface):
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
169
|
+
def get_llm(self):
|
|
170
|
+
from langchain_google_genai.chat_models import ChatGoogleGenerativeAI
|
|
171
|
+
return ChatGoogleGenerativeAI(
|
|
172
|
+
model=self.config.model,
|
|
173
|
+
google_api_key=self.config.api_key or os.getenv("GOOGLE_API_KEY"),
|
|
174
|
+
temperature=self.config.temperature,
|
|
175
|
+
disable_streaming=False,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def get_embeddings(self):
|
|
179
|
+
from langchain_google_genai.embeddings import GoogleGenerativeAIEmbeddings
|
|
180
|
+
return GoogleGenerativeAIEmbeddings(
|
|
181
|
+
google_api_key=self.config.api_key or os.getenv("GOOGLE_API_KEY"),
|
|
182
|
+
model="models/gemini-embedding-001")
|
|
183
|
+
|
|
184
|
+
def get_models(self):
|
|
185
|
+
import google.generativeai as genai
|
|
186
|
+
genai.configure(api_key=self.config.api_key or os.getenv("GOOGLE_API_KEY"))
|
|
187
|
+
response = genai.list_models()
|
|
188
|
+
return [{
|
|
189
|
+
"id": model.name,
|
|
190
|
+
"name": model.display_name,
|
|
191
|
+
"description": model.description,
|
|
192
|
+
"input_token_limit": model.input_token_limit,
|
|
193
|
+
"output_token_limit": model.output_token_limit
|
|
194
|
+
} for model in response if "gemini" in model.name.lower()]
|
|
195
|
+
|
|
196
|
+
async def _format_multimodal_file_message(self, message: dict, file: Base64File = None) -> dict:
|
|
197
|
+
_file = file or await Base64File.from_url(message.get("url"))
|
|
198
|
+
if _file.extension in ["pdf", "csv"]:
|
|
199
|
+
return {"type": "media", "mime_type": _file.mime_type, "data": _file.base64_content }
|
|
200
|
+
else:
|
|
201
|
+
return await super()._format_multimodal_file_message(message, _file)
|
|
202
|
+
|
|
203
|
+
class GoogleVertex(LlmInterface):
|
|
112
204
|
def get_llm(self):
|
|
113
205
|
from langchain_google_vertexai import ChatVertexAI
|
|
114
206
|
return ChatVertexAI(
|
|
@@ -116,47 +208,37 @@ class Gvertex(LlmInterface):
|
|
|
116
208
|
temperature=self.config.temperature
|
|
117
209
|
)
|
|
118
210
|
def get_embeddings(self):
|
|
119
|
-
from langchain_google_vertexai import VertexAIEmbeddings
|
|
120
|
-
|
|
211
|
+
from langchain_google_vertexai.embeddings import VertexAIEmbeddings
|
|
212
|
+
embeddings = VertexAIEmbeddings(model_name="gemini-embedding-001")
|
|
213
|
+
return embeddings
|
|
121
214
|
def get_models(self):
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
215
|
+
_models = [
|
|
216
|
+
{"id":"gemini-2.5-pro"},
|
|
217
|
+
{"id":"gemini-2.5-flash"},
|
|
218
|
+
{"id":"gemini-2.0-flash"},
|
|
219
|
+
{"id":"gemini-2.0-flash-lite"}
|
|
220
|
+
]
|
|
221
|
+
try:
|
|
222
|
+
from google.cloud import aiplatform
|
|
223
|
+
aiplatform.init()
|
|
224
|
+
_list = aiplatform.Model.list()
|
|
225
|
+
if _list:
|
|
226
|
+
_models = list([{"id": model.name} for model in _list])
|
|
125
227
|
# removed due issue: https://github.com/langchain-ai/langchain-google/issues/733
|
|
126
228
|
# Message type "google.cloud.aiplatform.v1beta1.GenerateContentResponse" has no field named "createTime" at "GenerateContentResponse". Available Fields(except extensions): "['candidates', 'modelVersion', 'promptFeedback', 'usageMetadata']"
|
|
229
|
+
except Exception as e:
|
|
230
|
+
print(f"Error fetching models from Gvertex: {e}")
|
|
231
|
+
# fallback to hardcoded models
|
|
232
|
+
#see https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations#united-states for available models
|
|
233
|
+
finally:
|
|
234
|
+
return _models
|
|
127
235
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
]
|
|
135
|
-
|
|
136
|
-
class Anthropic(LlmInterface):
|
|
137
|
-
def get_llm(self):
|
|
138
|
-
from langchain_anthropic import ChatAnthropic
|
|
139
|
-
return ChatAnthropic(
|
|
140
|
-
api_key=self.config.api_key or os.getenv("ANTHROPIC_API_KEY"),
|
|
141
|
-
model=self.config.model,
|
|
142
|
-
temperature=self.config.temperature,
|
|
143
|
-
streaming=True,
|
|
144
|
-
stream_usage=True
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
"""
|
|
148
|
-
def get_embeddings(self):
|
|
149
|
-
from langchain_voyageai import VoyageAIEmbeddings
|
|
150
|
-
return VoyageAIEmbeddings(
|
|
151
|
-
api_key=self.config.embedding_api_key, #voyage api key
|
|
152
|
-
model="voyage-3")
|
|
153
|
-
"""
|
|
154
|
-
|
|
155
|
-
def get_models(self):
|
|
156
|
-
import anthropic
|
|
157
|
-
client = anthropic.Client(api_key=self.config.api_key or os.getenv("ANTHROPIC_API_KEY"))
|
|
158
|
-
response = client.models.list()
|
|
159
|
-
return response.data
|
|
236
|
+
async def _format_multimodal_file_message(self, message: dict, file: Base64File = None) -> dict:
|
|
237
|
+
_file = file or await Base64File.from_url(message.get("url"))
|
|
238
|
+
if _file.extension in ["pdf", "csv"]:
|
|
239
|
+
return {"type": "media", "mime_type": _file.mime_type, "data": _file.base64_content }
|
|
240
|
+
else:
|
|
241
|
+
return await super()._format_multimodal_file_message(message, _file)
|
|
160
242
|
|
|
161
243
|
class Groq(LlmInterface):
|
|
162
244
|
def get_llm(self):
|
|
@@ -179,10 +261,74 @@ class Groq(LlmInterface):
|
|
|
179
261
|
response = requests.get(url, headers=headers)
|
|
180
262
|
return response.json().get("data", [])
|
|
181
263
|
|
|
264
|
+
class IBM(LlmInterface):
|
|
265
|
+
def __init__(self, config: LlmConfig):
|
|
266
|
+
from ibm_watsonx_ai import APIClient,Credentials
|
|
267
|
+
super().__init__(config)
|
|
268
|
+
self.__base_url = self.config.api_url or os.getenv("WATSONX_URL") or "https://us-south.ml.cloud.ibm.com"
|
|
269
|
+
self.__api_key = self.config.api_key or os.getenv("WATSONX_APIKEY")
|
|
270
|
+
self.__client = APIClient(
|
|
271
|
+
credentials=Credentials(url=self.__base_url,api_key=self.__api_key),
|
|
272
|
+
project_id=os.getenv("WATSONX_PROJECTID") or "default"
|
|
273
|
+
)
|
|
274
|
+
def get_llm(self):
|
|
275
|
+
from langchain_ibm import ChatWatsonx
|
|
276
|
+
return ChatWatsonx(
|
|
277
|
+
model_id=self.config.model,
|
|
278
|
+
watsonx_client=self.__client
|
|
279
|
+
)
|
|
280
|
+
def get_models(self):
|
|
281
|
+
import requests
|
|
282
|
+
from datetime import date
|
|
283
|
+
try:
|
|
284
|
+
# https://cloud.ibm.com/apidocs/watsonx-ai#list-foundation-model-specs
|
|
285
|
+
today = date.today().strftime("%Y-%m-%d")
|
|
286
|
+
url = f"{self.__base_url}/ml/v1/foundation_model_specs?version={today}&filters=task_generation,task_summarization:and"
|
|
287
|
+
headers = {
|
|
288
|
+
"Authorization": f"Bearer {self.__api_key}",
|
|
289
|
+
"Content-Type": "application/json"
|
|
290
|
+
}
|
|
291
|
+
response = requests.get(url, headers=headers)
|
|
292
|
+
models = response.json().get("resources", [])
|
|
293
|
+
return [{
|
|
294
|
+
"id": model['model_id'],
|
|
295
|
+
"provider": model['provider'],
|
|
296
|
+
"tasks": model['task_ids'],
|
|
297
|
+
"limits": model.get('model_limits', {}),
|
|
298
|
+
} for model in models]
|
|
299
|
+
except Exception as e:
|
|
300
|
+
print(f"Error fetching models from IBM WatsonX: {e}")
|
|
301
|
+
# https://www.ibm.com/products/watsonx-ai/foundation-models
|
|
302
|
+
return [
|
|
303
|
+
{"id":"ibm/granite-13b-instruct-v2"},
|
|
304
|
+
{"id":"ibm/granite-3-2b-instruct"},
|
|
305
|
+
{"id":"ibm/granite-3-8b-instruct"},
|
|
306
|
+
{"id":"meta-llama/llama-2-13b-chat"},
|
|
307
|
+
{"id":"meta-llama/llama-3-3-70b-instruct"},
|
|
308
|
+
{"id":"meta-llama/llama-4-maverick-17b-128e-instruct-fp8"},
|
|
309
|
+
{"id":"mistralai/mistral-large"},
|
|
310
|
+
{"id":"mistralai/mixtral-8x7b-instruct-v01"},
|
|
311
|
+
{"id":"mistralai/pixtral-12b"}
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
def get_embeddings(self):
|
|
315
|
+
from langchain_ibm import WatsonxEmbeddings
|
|
316
|
+
from ibm_watsonx_ai.metanames import EmbedTextParamsMetaNames
|
|
317
|
+
# https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/fm-models-embed.html?context=wx&audience=wdp#embed
|
|
318
|
+
embed_params = {
|
|
319
|
+
EmbedTextParamsMetaNames.TRUNCATE_INPUT_TOKENS: 512,
|
|
320
|
+
#EmbedTextParamsMetaNames.RETURN_OPTIONS: {"input_text": True},
|
|
321
|
+
}
|
|
322
|
+
return WatsonxEmbeddings(
|
|
323
|
+
model_id="ibm/granite-embedding-107m-multilingual", #https://www.ibm.com/products/watsonx-ai/foundation-models
|
|
324
|
+
watsonx_client=self.__client,
|
|
325
|
+
params=embed_params
|
|
326
|
+
)
|
|
327
|
+
|
|
182
328
|
class Ollama(LlmInterface):
|
|
183
329
|
def __init__(self, config: LlmConfig):
|
|
184
330
|
super().__init__(config)
|
|
185
|
-
self.__base_url = self.config.api_url or os.getenv("OLLAMA_API_URL")
|
|
331
|
+
self.__base_url = self.config.api_url or os.getenv("OLLAMA_API_URL") or "http://localhost:11434"
|
|
186
332
|
def get_llm(self):
|
|
187
333
|
from langchain_ollama.chat_models import ChatOllama
|
|
188
334
|
return ChatOllama(
|
|
@@ -195,7 +341,7 @@ class Ollama(LlmInterface):
|
|
|
195
341
|
from langchain_ollama.embeddings import OllamaEmbeddings
|
|
196
342
|
return OllamaEmbeddings(
|
|
197
343
|
base_url=self.__base_url,
|
|
198
|
-
model="
|
|
344
|
+
model="mxbai-embed-large" #nomic-embed-text
|
|
199
345
|
)
|
|
200
346
|
def get_models(self):
|
|
201
347
|
import requests
|
|
@@ -212,15 +358,27 @@ class Ollama(LlmInterface):
|
|
|
212
358
|
"details": model['details']
|
|
213
359
|
} for model in models]
|
|
214
360
|
|
|
361
|
+
async def _format_multimodal_image_message(self, message: dict) -> dict:
|
|
362
|
+
file = await Base64File.from_url(message.get("url"))
|
|
363
|
+
return { "type": "image_url", "image_url": { "url": file.base64_url }}
|
|
364
|
+
|
|
215
365
|
class LlmManager:
|
|
366
|
+
"""
|
|
367
|
+
Expose the available LLM providers.
|
|
368
|
+
Names are aligned with the LangChain documentation:
|
|
369
|
+
https://python.langchain.com/api_reference/langchain/chat_models/langchain.chat_models.base.init_chat_model.html
|
|
370
|
+
"""
|
|
216
371
|
|
|
217
372
|
#class variables (static)
|
|
218
373
|
_list: dict[str,LlmInterface] = {
|
|
219
374
|
"anthropic": Anthropic,
|
|
220
375
|
"deepseek": DeepSeek,
|
|
221
|
-
"google": Google,
|
|
222
|
-
"
|
|
376
|
+
"google": Google, #deprecated
|
|
377
|
+
"google_genai": Google,
|
|
378
|
+
"gvertex": GoogleVertex,#deprecated
|
|
379
|
+
"google_vertexai": GoogleVertex,
|
|
223
380
|
"groq": Groq,
|
|
381
|
+
"ibm": IBM,
|
|
224
382
|
"openai": OpenAI,
|
|
225
383
|
"ollama": Ollama
|
|
226
384
|
}
|
|
@@ -18,7 +18,7 @@ async def __process_proxy_tool(proxy_tool: LlmAppTool) -> LlmAppTool | None:
|
|
|
18
18
|
if not app:
|
|
19
19
|
raise ValueError(f"App with id {app_id} not found.")
|
|
20
20
|
tool_id = secrets.get("toolId")
|
|
21
|
-
tool = next((t for t in app.app_tools if t.id == tool_id), None)
|
|
21
|
+
tool = next((t for t in app.rq.app_tools if app.rq.app_tools and t.id == tool_id), None)
|
|
22
22
|
if not tool:
|
|
23
23
|
raise ValueError(f"Tool with function_id {tool_id} not found in app {app.name}.")
|
|
24
24
|
#override derived tool with proxy tool props
|
|
@@ -61,5 +61,8 @@ def get_structured_tools(llm: LlmInterface, tools: list[LlmAppTool], callbacks:l
|
|
|
61
61
|
#error_on_invalid_docstring=True
|
|
62
62
|
)
|
|
63
63
|
_structured_tool.tags = [tool.function_id if tool.function_id else tool.function_name]
|
|
64
|
+
secrets = tool.secrets_to_dict()
|
|
65
|
+
if secrets and secrets.get("stream") == "true":
|
|
66
|
+
_structured_tool.tags.append("stream")
|
|
64
67
|
_structured_tools.append(_structured_tool)
|
|
65
68
|
return _structured_tools
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from asyncio import Queue
|
|
2
|
-
import aiohttp
|
|
2
|
+
import aiohttp, re
|
|
3
3
|
from typing import Optional, Type, Callable
|
|
4
4
|
from ws_bom_robot_app.config import config
|
|
5
5
|
from ws_bom_robot_app.llm.models.api import LlmApp,LlmAppTool
|
|
@@ -128,7 +128,9 @@ class ToolManager:
|
|
|
128
128
|
from langchain_core.prompts import ChatPromptTemplate
|
|
129
129
|
from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
|
|
130
130
|
from pydantic import create_model
|
|
131
|
-
system_message = self.app_tool.llm_chain_settings.prompt
|
|
131
|
+
system_message = self.app_tool.llm_chain_settings.prompt.format(
|
|
132
|
+
thread_id = self.app_tool.thread_id if self.app_tool.thread_id else "no-thread-id",
|
|
133
|
+
)
|
|
132
134
|
context = []
|
|
133
135
|
if self.app_tool.data_source == "knowledgebase":
|
|
134
136
|
context = await self.__extract_documents(input, self.app_tool)
|
|
@@ -166,6 +168,7 @@ class ToolManager:
|
|
|
166
168
|
return result
|
|
167
169
|
|
|
168
170
|
async def proxy_app_chat(self, query: str) -> str | None:
|
|
171
|
+
from ws_bom_robot_app.llm.models.api import LlmMessage
|
|
169
172
|
secrets = self.app_tool.secrets_to_dict()
|
|
170
173
|
app_id = secrets.get("appId")
|
|
171
174
|
if not app_id:
|
|
@@ -173,26 +176,49 @@ class ToolManager:
|
|
|
173
176
|
app: CmsApp = await get_app_by_id(app_id)
|
|
174
177
|
if not app:
|
|
175
178
|
raise ValueError(f"App with id {app_id} not found.")
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
179
|
+
# message
|
|
180
|
+
app.rq.messages.append(LlmMessage(role="user", content=query))
|
|
181
|
+
# tracing
|
|
182
|
+
if str(secrets.get("disable_tracing", False)).lower() in ['1','true','yes']:
|
|
183
|
+
app.rq.lang_chain_tracing = False
|
|
184
|
+
app.rq.lang_chain_project = ''
|
|
185
|
+
app.rq.secrets['nebulyApiKey'] = ''
|
|
186
|
+
# http: for debugging purposes
|
|
187
|
+
if str(secrets.get("use_http", False)).lower() in ['1','true','yes']:
|
|
188
|
+
import base64
|
|
189
|
+
url = f"http://localhost:{config.runtime_options().tcp_port}/api/llm/stream/raw"
|
|
190
|
+
auth = f"Basic {base64.b64encode((config.robot_user + ':' + config.robot_password).encode('utf-8')).decode('utf-8')}"
|
|
191
|
+
headers = {"Authorization": auth} if auth else {}
|
|
192
|
+
async with aiohttp.ClientSession() as session:
|
|
193
|
+
_data = app.rq.model_dump(mode='json',by_alias=True,exclude_unset=True,exclude_none=True, exclude_defaults=True)
|
|
194
|
+
async with session.post(url, json=_data, headers=headers) as response:
|
|
195
|
+
if response.status == 200:
|
|
196
|
+
return await response.text()
|
|
197
|
+
else:
|
|
198
|
+
raise ValueError(f"Error fetching chat response: {response.status}")
|
|
199
|
+
return None
|
|
200
|
+
else: # default
|
|
201
|
+
try:
|
|
202
|
+
from ws_bom_robot_app.llm.main import stream
|
|
203
|
+
import json
|
|
204
|
+
chunks = []
|
|
205
|
+
async for chunk in stream(rq=app.rq, ctx=None, formatted=False):
|
|
206
|
+
chunks.append(chunk)
|
|
207
|
+
rs = ''.join(chunks) if chunks else None
|
|
208
|
+
|
|
209
|
+
# if the app has output_structure, parse the JSON and return dict
|
|
210
|
+
if rs and app.rq.output_structure:
|
|
211
|
+
try:
|
|
212
|
+
cleaned_rs = re.sub(r'^```(?:json)?\s*\n?', '', rs.strip())
|
|
213
|
+
cleaned_rs = re.sub(r'\n?```\s*$', '', cleaned_rs)
|
|
214
|
+
return json.loads(cleaned_rs)
|
|
215
|
+
except json.JSONDecodeError:
|
|
216
|
+
print(f"[!] Failed to parse JSON output from proxy_app_chat: {rs}")
|
|
217
|
+
return rs
|
|
218
|
+
return rs
|
|
219
|
+
except Exception as e:
|
|
220
|
+
print(f"[!] Error in proxy_app_chat: {e}")
|
|
221
|
+
return None
|
|
196
222
|
|
|
197
223
|
async def proxy_app_tool(self) -> None:
|
|
198
224
|
return None
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
from langchain_core.documents import Document
|
|
2
2
|
from langchain_text_splitters import CharacterTextSplitter
|
|
3
|
+
import logging
|
|
3
4
|
|
|
4
5
|
class DocumentChunker:
|
|
6
|
+
_MAX_CHUNK_SIZE = 10_000
|
|
5
7
|
@staticmethod
|
|
6
8
|
def chunk(documents: list[Document]) -> list[Document]:
|
|
7
|
-
text_splitter = CharacterTextSplitter(chunk_size=
|
|
9
|
+
text_splitter = CharacterTextSplitter(chunk_size=DocumentChunker._MAX_CHUNK_SIZE, chunk_overlap=int(DocumentChunker._MAX_CHUNK_SIZE * 0.02))
|
|
8
10
|
chunked_documents = []
|
|
9
11
|
for doc in documents:
|
|
12
|
+
if len(doc.page_content) <= DocumentChunker._MAX_CHUNK_SIZE:
|
|
13
|
+
chunked_documents.append(doc)
|
|
14
|
+
continue
|
|
10
15
|
chunks = text_splitter.split_text(doc.page_content)
|
|
11
16
|
for chunk in chunks:
|
|
12
17
|
chunked_documents.append(
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import os, logging
|
|
2
|
+
from ws_bom_robot_app.config import config
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
|
|
5
|
+
def _cleanup_data_file(folders: list[str], retention: float) -> dict:
|
|
6
|
+
"""
|
|
7
|
+
clean up old data files in the specified folder
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
- Dictionary with cleanup statistics
|
|
11
|
+
"""
|
|
12
|
+
_deleted_files = []
|
|
13
|
+
_deleted_dirs = []
|
|
14
|
+
_freed_space = 0
|
|
15
|
+
|
|
16
|
+
for folder in folders:
|
|
17
|
+
if not os.path.exists(folder):
|
|
18
|
+
logging.warning(f"Folder does not exist: {folder}")
|
|
19
|
+
continue
|
|
20
|
+
|
|
21
|
+
# delete old files
|
|
22
|
+
for root, dirs, files in os.walk(folder, topdown=False):
|
|
23
|
+
for file in files:
|
|
24
|
+
file_path = os.path.join(root, file)
|
|
25
|
+
try:
|
|
26
|
+
file_stat = os.stat(file_path)
|
|
27
|
+
file_creation_time = datetime.fromtimestamp(file_stat.st_mtime)
|
|
28
|
+
if file_creation_time < datetime.now() - timedelta(days=retention):
|
|
29
|
+
_freed_space += file_stat.st_size
|
|
30
|
+
os.remove(file_path)
|
|
31
|
+
_deleted_files.append(file_path)
|
|
32
|
+
except (OSError, IOError) as e:
|
|
33
|
+
logging.error(f"Error deleting file {file_path}: {e}")
|
|
34
|
+
|
|
35
|
+
# clean up empty directories (bottom-up)
|
|
36
|
+
for root, dirs, files in os.walk(folder, topdown=False):
|
|
37
|
+
# skip the root folder itself
|
|
38
|
+
if root == folder:
|
|
39
|
+
continue
|
|
40
|
+
try:
|
|
41
|
+
# check if directory is empty
|
|
42
|
+
if not os.listdir(root):
|
|
43
|
+
os.rmdir(root)
|
|
44
|
+
_deleted_dirs.append(root)
|
|
45
|
+
except OSError as e:
|
|
46
|
+
logging.debug(f"Could not remove directory {root}: {e}")
|
|
47
|
+
logging.info(f"Deleted {len(_deleted_files)} files; Freed space: {_freed_space / (1024 * 1024):.2f} MB")
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
"deleted_files_count": len(_deleted_files),
|
|
51
|
+
"deleted_dirs_count": len(_deleted_dirs),
|
|
52
|
+
"freed_space_mb": _freed_space / (1024 * 1024)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def kb_cleanup_data_file() -> dict:
|
|
56
|
+
"""
|
|
57
|
+
clean up vector db data files
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
folders = [
|
|
61
|
+
os.path.join(config.robot_data_folder, config.robot_data_db_folder, config.robot_data_db_folder_out),
|
|
62
|
+
os.path.join(config.robot_data_folder, config.robot_data_db_folder, config.robot_data_db_folder_store),
|
|
63
|
+
os.path.join(config.robot_data_folder, config.robot_data_db_folder, config.robot_data_db_folder_src)
|
|
64
|
+
]
|
|
65
|
+
return _cleanup_data_file(folders, config.robot_data_db_retention_days)
|
|
66
|
+
|
|
67
|
+
def chat_cleanup_attachment() -> dict:
|
|
68
|
+
"""
|
|
69
|
+
clean up chat attachment files
|
|
70
|
+
"""
|
|
71
|
+
folders = [
|
|
72
|
+
os.path.join(config.robot_data_folder, config.robot_data_attachment_folder)
|
|
73
|
+
]
|
|
74
|
+
return _cleanup_data_file(folders, config.robot_data_attachment_retention_days)
|
|
75
|
+
|
|
76
|
+
def task_cleanup_history() -> None:
|
|
77
|
+
"""
|
|
78
|
+
clean up task queue
|
|
79
|
+
"""
|
|
80
|
+
from ws_bom_robot_app.task_manager import task_manager
|
|
81
|
+
task_manager.cleanup_task()
|