ws-bom-robot-app 0.0.37__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.
Files changed (66) hide show
  1. ws_bom_robot_app/config.py +35 -7
  2. ws_bom_robot_app/cron_manager.py +15 -14
  3. ws_bom_robot_app/llm/agent_context.py +26 -0
  4. ws_bom_robot_app/llm/agent_description.py +123 -123
  5. ws_bom_robot_app/llm/agent_handler.py +176 -180
  6. ws_bom_robot_app/llm/agent_lcel.py +107 -54
  7. ws_bom_robot_app/llm/api.py +100 -7
  8. ws_bom_robot_app/llm/defaut_prompt.py +15 -15
  9. ws_bom_robot_app/llm/evaluator.py +319 -0
  10. ws_bom_robot_app/llm/feedbacks/__init__.py +0 -0
  11. ws_bom_robot_app/llm/feedbacks/feedback_manager.py +66 -0
  12. ws_bom_robot_app/llm/main.py +159 -110
  13. ws_bom_robot_app/llm/models/api.py +70 -5
  14. ws_bom_robot_app/llm/models/feedback.py +30 -0
  15. ws_bom_robot_app/llm/nebuly_handler.py +185 -0
  16. ws_bom_robot_app/llm/providers/llm_manager.py +244 -80
  17. ws_bom_robot_app/llm/tools/models/main.py +8 -0
  18. ws_bom_robot_app/llm/tools/tool_builder.py +68 -23
  19. ws_bom_robot_app/llm/tools/tool_manager.py +343 -133
  20. ws_bom_robot_app/llm/tools/utils.py +41 -25
  21. ws_bom_robot_app/llm/utils/agent.py +34 -0
  22. ws_bom_robot_app/llm/utils/chunker.py +6 -1
  23. ws_bom_robot_app/llm/utils/cleanup.py +81 -0
  24. ws_bom_robot_app/llm/utils/cms.py +123 -0
  25. ws_bom_robot_app/llm/utils/download.py +183 -79
  26. ws_bom_robot_app/llm/utils/print.py +29 -29
  27. ws_bom_robot_app/llm/vector_store/db/__init__.py +0 -0
  28. ws_bom_robot_app/llm/vector_store/db/base.py +193 -0
  29. ws_bom_robot_app/llm/vector_store/db/chroma.py +97 -0
  30. ws_bom_robot_app/llm/vector_store/db/faiss.py +91 -0
  31. ws_bom_robot_app/llm/vector_store/db/manager.py +15 -0
  32. ws_bom_robot_app/llm/vector_store/db/qdrant.py +73 -0
  33. ws_bom_robot_app/llm/vector_store/generator.py +137 -137
  34. ws_bom_robot_app/llm/vector_store/integration/api.py +216 -0
  35. ws_bom_robot_app/llm/vector_store/integration/azure.py +1 -1
  36. ws_bom_robot_app/llm/vector_store/integration/base.py +58 -15
  37. ws_bom_robot_app/llm/vector_store/integration/confluence.py +41 -11
  38. ws_bom_robot_app/llm/vector_store/integration/dropbox.py +1 -1
  39. ws_bom_robot_app/llm/vector_store/integration/gcs.py +1 -1
  40. ws_bom_robot_app/llm/vector_store/integration/github.py +22 -22
  41. ws_bom_robot_app/llm/vector_store/integration/googledrive.py +46 -17
  42. ws_bom_robot_app/llm/vector_store/integration/jira.py +112 -75
  43. ws_bom_robot_app/llm/vector_store/integration/manager.py +6 -2
  44. ws_bom_robot_app/llm/vector_store/integration/s3.py +1 -1
  45. ws_bom_robot_app/llm/vector_store/integration/sftp.py +1 -1
  46. ws_bom_robot_app/llm/vector_store/integration/sharepoint.py +7 -14
  47. ws_bom_robot_app/llm/vector_store/integration/shopify.py +143 -0
  48. ws_bom_robot_app/llm/vector_store/integration/sitemap.py +9 -1
  49. ws_bom_robot_app/llm/vector_store/integration/slack.py +3 -2
  50. ws_bom_robot_app/llm/vector_store/integration/thron.py +236 -0
  51. ws_bom_robot_app/llm/vector_store/loader/base.py +52 -8
  52. ws_bom_robot_app/llm/vector_store/loader/docling.py +71 -33
  53. ws_bom_robot_app/llm/vector_store/loader/json_loader.py +25 -25
  54. ws_bom_robot_app/main.py +148 -146
  55. ws_bom_robot_app/subprocess_runner.py +106 -0
  56. ws_bom_robot_app/task_manager.py +207 -54
  57. ws_bom_robot_app/util.py +65 -20
  58. ws_bom_robot_app-0.0.103.dist-info/METADATA +364 -0
  59. ws_bom_robot_app-0.0.103.dist-info/RECORD +76 -0
  60. {ws_bom_robot_app-0.0.37.dist-info → ws_bom_robot_app-0.0.103.dist-info}/WHEEL +1 -1
  61. ws_bom_robot_app/llm/settings.py +0 -4
  62. ws_bom_robot_app/llm/utils/agent_utils.py +0 -17
  63. ws_bom_robot_app/llm/utils/kb.py +0 -34
  64. ws_bom_robot_app-0.0.37.dist-info/METADATA +0 -277
  65. ws_bom_robot_app-0.0.37.dist-info/RECORD +0 -60
  66. {ws_bom_robot_app-0.0.37.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):
@@ -44,8 +115,12 @@ class OpenAI(LlmInterface):
44
115
 
45
116
  def get_llm(self):
46
117
  from langchain_openai import ChatOpenAI
47
- chat = ChatOpenAI(api_key=self.config.api_key, model=self.config.model)
48
- if not any(self.config.model.startswith(prefix) for prefix in ["o1", "o3"]):
118
+ chat = ChatOpenAI(
119
+ api_key=self.config.api_key or os.getenv("OPENAI_API_KEY"),
120
+ model=self.config.model,
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):
49
124
  chat.temperature = self.config.temperature
50
125
  chat.streaming = True
51
126
  return chat
@@ -56,54 +131,76 @@ class OpenAI(LlmInterface):
56
131
  response = openai.models.list()
57
132
  return response.data
58
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
+
59
141
  class DeepSeek(LlmInterface):
60
142
  def get_llm(self):
61
143
  from langchain_openai import ChatOpenAI
62
144
  return ChatOpenAI(
63
- api_key=self.config.api_key,
145
+ api_key=self.config.api_key or os.getenv("DEEPSEEK_API_KEY"),
64
146
  model=self.config.model,
65
- base_url="https://api.deepseek.com/v1",
147
+ base_url="https://api.deepseek.com",
66
148
  max_tokens=8192,
67
149
  temperature=self.config.temperature,
68
- streaming=True,
150
+ streaming=True
69
151
  )
70
152
 
71
153
  def get_models(self):
72
- return [
73
- {"id":"deepseek-chat"},
74
- {"id":"deepseek-reasoner"}
75
- ]
154
+ import openai
155
+ openai.api_key = self.config.api_key or os.getenv("DEEPSEEK_API_KEY")
156
+ openai.base_url = "https://api.deepseek.com"
157
+ response = openai.models.list()
158
+ return response.data
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
76
167
 
77
168
  class Google(LlmInterface):
78
- def get_llm(self):
79
- from langchain_google_genai.chat_models import ChatGoogleGenerativeAI
80
- return ChatGoogleGenerativeAI(
81
- name="chat",
82
- api_key=self.config.api_key,
83
- model=self.config.model,
84
- temperature=self.config.temperature,
85
- disable_streaming=False
86
- )
87
-
88
- def get_embeddings(self):
89
- from langchain_google_genai.embeddings import GoogleGenerativeAIEmbeddings
90
- return GoogleGenerativeAIEmbeddings(
91
- google_api_key=self.config.api_key,
92
- model="models/text-embedding-004")
93
-
94
- def get_models(self):
95
- import google.generativeai as genai
96
- genai.configure(api_key=self.config.api_key or os.getenv("GOOGLE_API_KEY"))
97
- response = genai.list_models()
98
- return [{
99
- "id": model.name,
100
- "name": model.display_name,
101
- "description": model.description,
102
- "input_token_limit": model.input_token_limit,
103
- "output_token_limit": model.output_token_limit
104
- } for model in response if "gemini" in model.name.lower()]
105
-
106
- class Gvertex(LlmInterface):
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):
107
204
  def get_llm(self):
108
205
  from langchain_google_vertexai import ChatVertexAI
109
206
  return ChatVertexAI(
@@ -111,52 +208,43 @@ class Gvertex(LlmInterface):
111
208
  temperature=self.config.temperature
112
209
  )
113
210
  def get_embeddings(self):
114
- from langchain_google_vertexai import VertexAIEmbeddings
115
- return VertexAIEmbeddings(model_name="text-embedding-004")
211
+ from langchain_google_vertexai.embeddings import VertexAIEmbeddings
212
+ embeddings = VertexAIEmbeddings(model_name="gemini-embedding-001")
213
+ return embeddings
116
214
  def get_models(self):
117
- #from google.cloud import aiplatform
118
- #aiplatform.init()
119
- #models = aiplatform.Model.list()
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])
120
227
  # removed due issue: https://github.com/langchain-ai/langchain-google/issues/733
121
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
122
235
 
123
- #see https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations#united-states for available models
124
- return [
125
- {"id":"gemini-2.0-flash-001"},
126
- {"id":"gemini-1.5-pro-001"},
127
- {"id":"gemini-1.5-pro-002"}
128
- ]
129
-
130
- class Anthropic(LlmInterface):
131
- def get_llm(self):
132
- from langchain_anthropic import ChatAnthropic
133
- return ChatAnthropic(
134
- api_key=self.config.api_key,
135
- model=self.config.model,
136
- temperature=self.config.temperature,
137
- streaming=True,
138
- stream_usage=False
139
- )
140
-
141
- """
142
- def get_embeddings(self):
143
- from langchain_voyageai import VoyageAIEmbeddings
144
- return VoyageAIEmbeddings(
145
- api_key=self.config.embedding_api_key, #voyage api key
146
- model="voyage-3")
147
- """
148
-
149
- def get_models(self):
150
- import anthropic
151
- client = anthropic.Client(api_key=self.config.api_key or os.getenv("ANTHROPIC_API_KEY"))
152
- response = client.models.list()
153
- 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)
154
242
 
155
243
  class Groq(LlmInterface):
156
244
  def get_llm(self):
157
245
  from langchain_groq import ChatGroq
158
246
  return ChatGroq(
159
- api_key=self.config.api_key,
247
+ api_key=self.config.api_key or os.getenv("GROQ_API_KEY"),
160
248
  model=self.config.model,
161
249
  #max_tokens=8192,
162
250
  temperature=self.config.temperature,
@@ -173,10 +261,74 @@ class Groq(LlmInterface):
173
261
  response = requests.get(url, headers=headers)
174
262
  return response.json().get("data", [])
175
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
+
176
328
  class Ollama(LlmInterface):
177
329
  def __init__(self, config: LlmConfig):
178
330
  super().__init__(config)
179
- 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"
180
332
  def get_llm(self):
181
333
  from langchain_ollama.chat_models import ChatOllama
182
334
  return ChatOllama(
@@ -189,7 +341,7 @@ class Ollama(LlmInterface):
189
341
  from langchain_ollama.embeddings import OllamaEmbeddings
190
342
  return OllamaEmbeddings(
191
343
  base_url=self.__base_url,
192
- model="nomic-embed-text" #mxbai-embed-large
344
+ model="mxbai-embed-large" #nomic-embed-text
193
345
  )
194
346
  def get_models(self):
195
347
  import requests
@@ -206,15 +358,27 @@ class Ollama(LlmInterface):
206
358
  "details": model['details']
207
359
  } for model in models]
208
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
+
209
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
+ """
210
371
 
211
372
  #class variables (static)
212
373
  _list: dict[str,LlmInterface] = {
213
374
  "anthropic": Anthropic,
214
375
  "deepseek": DeepSeek,
215
- "google": Google,
216
- "gvertex": Gvertex,
376
+ "google": Google, #deprecated
377
+ "google_genai": Google,
378
+ "gvertex": GoogleVertex,#deprecated
379
+ "google_vertexai": GoogleVertex,
217
380
  "groq": Groq,
381
+ "ibm": IBM,
218
382
  "openai": OpenAI,
219
383
  "ollama": Ollama
220
384
  }
@@ -7,3 +7,11 @@ class DocumentRetrieverInput(BaseModel):
7
7
  class ImageGeneratorInput(BaseModel):
8
8
  query: str = Field(description="description of the image to generate.")
9
9
  language: str = Field(description="Language of the query. Default is 'it'", default="it")
10
+ class LlmChainInput(BaseModel):
11
+ input: str = Field(description="Input to the LLM chain")
12
+ class SearchOnlineInput(BaseModel):
13
+ query: str = Field(description="The search query string")
14
+ class EmailSenderInput(BaseModel):
15
+ email_subject: str = Field(description="The subject of the email to send")
16
+ body: str = Field(description="The body of the email to send")
17
+ to_email: str = Field(description="The recipient email address")
@@ -1,23 +1,68 @@
1
- from asyncio import Queue
2
- from langchain.tools import Tool, StructuredTool
3
- from ws_bom_robot_app.llm.models.api import LlmAppTool
4
- from ws_bom_robot_app.llm.tools.tool_manager import ToolManager
5
- from ws_bom_robot_app.llm.providers.llm_manager import LlmInterface
6
-
7
- def get_structured_tools(llm: LlmInterface, tools: list[LlmAppTool], callbacks:list, queue: Queue) -> list[StructuredTool]:
8
- _structured_tools :list[StructuredTool] = []
9
- for tool in [tool for tool in tools if tool.is_active]:
10
- if _tool_config := ToolManager._list.get(tool.function_name):
11
- _tool_instance = ToolManager(llm, tool, callbacks, queue)
12
- _structured_tool = StructuredTool.from_function(
13
- coroutine=_tool_instance.get_coroutine(),
14
- name=tool.function_id if tool.function_id else tool.function_name,
15
- description=tool.function_description,
16
- args_schema=_tool_config.model
17
- #infer_schema=True,
18
- #parse_docstring=True,
19
- #error_on_invalid_docstring=True
20
- )
21
- _structured_tool.tags = [tool.function_id if tool.function_id else tool.function_name]
22
- _structured_tools.append(_structured_tool)
23
- return _structured_tools
1
+ import asyncio
2
+ from asyncio import Queue
3
+ from langchain.tools import StructuredTool
4
+ from ws_bom_robot_app.llm.models.api import LlmAppTool
5
+ from ws_bom_robot_app.llm.tools.tool_manager import ToolManager
6
+ from ws_bom_robot_app.llm.providers.llm_manager import LlmInterface
7
+
8
+ async def __process_proxy_tool(proxy_tool: LlmAppTool) -> LlmAppTool | None:
9
+ import os
10
+ from ws_bom_robot_app.llm.utils.cms import CmsApp, get_app_by_id
11
+ from ws_bom_robot_app.config import config
12
+ try:
13
+ secrets = proxy_tool.secrets_to_dict()
14
+ app_id = secrets.get("appId")
15
+ if not app_id:
16
+ raise ValueError("Tool configuration is invalid. 'appId' is required.")
17
+ app: CmsApp = await get_app_by_id(app_id)
18
+ if not app:
19
+ raise ValueError(f"App with id {app_id} not found.")
20
+ tool_id = secrets.get("toolId")
21
+ tool = next((t for t in app.rq.app_tools if app.rq.app_tools and t.id == tool_id), None)
22
+ if not tool:
23
+ raise ValueError(f"Tool with function_id {tool_id} not found in app {app.name}.")
24
+ #override derived tool with proxy tool props
25
+ tool.name = proxy_tool.name if proxy_tool.name else tool.name
26
+ tool.description = proxy_tool.description if proxy_tool.description else tool.description
27
+ tool.function_id = proxy_tool.function_id if proxy_tool.function_id else tool.function_id
28
+ tool.function_description = proxy_tool.function_description if proxy_tool.function_description else tool.function_description
29
+ #normalize vector_db
30
+ if tool.vector_db:
31
+ tool.vector_db = os.path.join(
32
+ os.path.join(config.robot_data_folder,config.robot_data_db_folder,config.robot_data_db_folder_store),
33
+ os.path.splitext(os.path.basename(tool.vector_db))[0]) if tool.vector_db else None
34
+ return tool
35
+ except Exception as e:
36
+ print(f"[!] Error in proxy_app_tool: {e}")
37
+ return None
38
+
39
+ def get_structured_tools(llm: LlmInterface, tools: list[LlmAppTool], callbacks:list, queue: Queue) -> list[StructuredTool]:
40
+ _structured_tools :list[StructuredTool] = []
41
+ for tool in [tool for tool in tools if tool.is_active]:
42
+ if tool.function_name == "proxy_app_tool":
43
+ # override the tool
44
+ loop = asyncio.get_event_loop()
45
+ if loop.is_running():
46
+ import nest_asyncio
47
+ nest_asyncio.apply()
48
+ processed_tool = loop.run_until_complete(__process_proxy_tool(tool))
49
+ if processed_tool is None:
50
+ continue
51
+ tool = processed_tool
52
+ if _tool_config := ToolManager._list.get(tool.function_name):
53
+ _tool_instance = ToolManager(llm, tool, callbacks, queue)
54
+ _structured_tool = StructuredTool.from_function(
55
+ coroutine=_tool_instance.get_coroutine(),
56
+ name=tool.function_id if tool.function_id else tool.function_name,
57
+ description=tool.function_description,
58
+ args_schema=_tool_config.model
59
+ #infer_schema=True,
60
+ #parse_docstring=True,
61
+ #error_on_invalid_docstring=True
62
+ )
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")
67
+ _structured_tools.append(_structured_tool)
68
+ return _structured_tools