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.
Files changed (51) hide show
  1. ws_bom_robot_app/config.py +30 -8
  2. ws_bom_robot_app/cron_manager.py +13 -12
  3. ws_bom_robot_app/llm/agent_context.py +1 -1
  4. ws_bom_robot_app/llm/agent_handler.py +11 -12
  5. ws_bom_robot_app/llm/agent_lcel.py +80 -18
  6. ws_bom_robot_app/llm/api.py +69 -7
  7. ws_bom_robot_app/llm/evaluator.py +319 -0
  8. ws_bom_robot_app/llm/main.py +51 -28
  9. ws_bom_robot_app/llm/models/api.py +40 -6
  10. ws_bom_robot_app/llm/nebuly_handler.py +18 -15
  11. ws_bom_robot_app/llm/providers/llm_manager.py +233 -75
  12. ws_bom_robot_app/llm/tools/tool_builder.py +4 -1
  13. ws_bom_robot_app/llm/tools/tool_manager.py +48 -22
  14. ws_bom_robot_app/llm/utils/chunker.py +6 -1
  15. ws_bom_robot_app/llm/utils/cleanup.py +81 -0
  16. ws_bom_robot_app/llm/utils/cms.py +60 -14
  17. ws_bom_robot_app/llm/utils/download.py +112 -8
  18. ws_bom_robot_app/llm/vector_store/db/base.py +50 -0
  19. ws_bom_robot_app/llm/vector_store/db/chroma.py +28 -8
  20. ws_bom_robot_app/llm/vector_store/db/faiss.py +35 -8
  21. ws_bom_robot_app/llm/vector_store/db/qdrant.py +29 -14
  22. ws_bom_robot_app/llm/vector_store/integration/api.py +216 -0
  23. ws_bom_robot_app/llm/vector_store/integration/azure.py +1 -1
  24. ws_bom_robot_app/llm/vector_store/integration/base.py +58 -15
  25. ws_bom_robot_app/llm/vector_store/integration/confluence.py +33 -5
  26. ws_bom_robot_app/llm/vector_store/integration/dropbox.py +1 -1
  27. ws_bom_robot_app/llm/vector_store/integration/gcs.py +1 -1
  28. ws_bom_robot_app/llm/vector_store/integration/github.py +22 -22
  29. ws_bom_robot_app/llm/vector_store/integration/googledrive.py +46 -17
  30. ws_bom_robot_app/llm/vector_store/integration/jira.py +93 -60
  31. ws_bom_robot_app/llm/vector_store/integration/manager.py +6 -2
  32. ws_bom_robot_app/llm/vector_store/integration/s3.py +1 -1
  33. ws_bom_robot_app/llm/vector_store/integration/sftp.py +1 -1
  34. ws_bom_robot_app/llm/vector_store/integration/sharepoint.py +7 -14
  35. ws_bom_robot_app/llm/vector_store/integration/shopify.py +143 -0
  36. ws_bom_robot_app/llm/vector_store/integration/sitemap.py +6 -1
  37. ws_bom_robot_app/llm/vector_store/integration/slack.py +3 -2
  38. ws_bom_robot_app/llm/vector_store/integration/thron.py +236 -0
  39. ws_bom_robot_app/llm/vector_store/loader/base.py +52 -8
  40. ws_bom_robot_app/llm/vector_store/loader/docling.py +71 -33
  41. ws_bom_robot_app/main.py +148 -146
  42. ws_bom_robot_app/subprocess_runner.py +106 -0
  43. ws_bom_robot_app/task_manager.py +204 -53
  44. ws_bom_robot_app/util.py +6 -0
  45. {ws_bom_robot_app-0.0.63.dist-info → ws_bom_robot_app-0.0.103.dist-info}/METADATA +158 -75
  46. ws_bom_robot_app-0.0.103.dist-info/RECORD +76 -0
  47. ws_bom_robot_app/llm/settings.py +0 -4
  48. ws_bom_robot_app/llm/utils/kb.py +0 -34
  49. ws_bom_robot_app-0.0.63.dist-info/RECORD +0 -72
  50. {ws_bom_robot_app-0.0.63.dist-info → ws_bom_robot_app-0.0.103.dist-info}/WHEEL +0 -0
  51. {ws_bom_robot_app-0.0.63.dist-info → ws_bom_robot_app-0.0.103.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,319 @@
1
+ from uuid import UUID
2
+ import requests, base64
3
+ from typing import Iterator, Optional, List, Union
4
+ from enum import Enum
5
+ from ws_bom_robot_app.config import config
6
+ from ws_bom_robot_app.llm.models.api import LlmMessage, StreamRequest
7
+ from langsmith import Client, traceable
8
+ from langsmith.schemas import Dataset, Example, Feedback, Run
9
+ from openevals.llm import create_llm_as_judge
10
+ from openevals.prompts import CORRECTNESS_PROMPT, RAG_HELPFULNESS_PROMPT, CONCISENESS_PROMPT, RAG_GROUNDEDNESS_PROMPT, HALLUCINATION_PROMPT
11
+ from pydantic import BaseModel
12
+
13
+ ls_client = Client()
14
+
15
+ class EvaluatorType(Enum):
16
+ """Available evaluator types"""
17
+ CORRECTNESS = "correctness"
18
+ HELPFULNESS = "helpfulness"
19
+ CONCISENESS = "conciseness"
20
+ RAG_GROUNDEDNESS = "rag_groundedness"
21
+ RAG_HALLUCINATION = "rag_hallucination"
22
+
23
+ @classmethod
24
+ def all(cls) -> List['EvaluatorType']:
25
+ """Get all available evaluator types"""
26
+ return list(cls)
27
+
28
+ @classmethod
29
+ def default(cls) -> List['EvaluatorType']:
30
+ """Get default evaluator types"""
31
+ return [cls.CORRECTNESS]
32
+
33
+ class EvaluatorDataSets:
34
+
35
+ @classmethod
36
+ def all(cls) -> List[Dataset]:
37
+ return list(ls_client.list_datasets())
38
+ @classmethod
39
+ def find(cls, name: str) -> List[Dataset]:
40
+ return [d for d in cls.all() if d.name.lower().__contains__(name.lower())]
41
+ @classmethod
42
+ def get(cls, id: Union[str, UUID]) -> Optional[Dataset]:
43
+ return next((d for d in cls.all() if str(d.id) == str(id)), None)
44
+ @classmethod
45
+ def create(cls, name: str) -> Dataset:
46
+ return ls_client.create_dataset(name=name)
47
+ @classmethod
48
+ def delete(cls, id: str) -> None:
49
+ ls_client.delete_dataset(id=id)
50
+ @classmethod
51
+ def example(cls, id: str) -> List[Example]:
52
+ return list(ls_client.list_examples(dataset_id=id, include_attachments=True))
53
+ @classmethod
54
+ def add_example(cls, dataset_id: str, inputs: dict, outputs: dict) -> Example:
55
+ """Add an example to the dataset.
56
+ Args:
57
+ inputs (dict): The input data for the example.
58
+ outputs (dict): The output data for the example.
59
+ Sample:
60
+ - inputs: {"question": "What is the capital of France?"}
61
+ outputs: {"answer": "Paris"}
62
+ """
63
+ return ls_client.create_example(dataset_id=dataset_id, inputs=inputs, outputs=outputs)
64
+ @classmethod
65
+ def feedback(cls, experiment_name: str) -> Iterator[Feedback]:
66
+ return ls_client.list_feedback(
67
+ run_ids=[r.id for r in ls_client.list_runs(project_name=experiment_name)]
68
+ )
69
+
70
+ class Evaluator:
71
+ def __init__(self, rq: StreamRequest, data: Union[Dataset,List[Example]], judge_model: Optional[str] = None):
72
+ """Evaluator class for assessing model performance.
73
+
74
+ Args:
75
+ rq (StreamRequest): The request object containing input data.
76
+ data (Union[Dataset, List[Example]]): The dataset to use for evaluation or a list of examples.
77
+ judge_model (Optional[str], optional): The model to use for evaluation, defaults to "openai:o4-mini".
78
+ For a list of available models, see the LangChain documentation:
79
+ https://python.langchain.com/api_reference/langchain/chat_models/langchain.chat_models.base.init_chat_model.html
80
+ """
81
+ self.judge_model: str = judge_model or "openai:o4-mini"
82
+ self.data = data
83
+ self.rq: StreamRequest = rq
84
+
85
+ #region evaluators
86
+
87
+ def _get_evaluator_function(self, evaluator_type: EvaluatorType):
88
+ """Get the evaluator function for a given type"""
89
+ evaluator_map = {
90
+ EvaluatorType.CORRECTNESS: self.correctness_evaluator,
91
+ EvaluatorType.HELPFULNESS: self.helpfulness_evaluator,
92
+ EvaluatorType.CONCISENESS: self.conciseness_evaluator,
93
+ EvaluatorType.RAG_GROUNDEDNESS: self.rag_groundedness_evaluator,
94
+ EvaluatorType.RAG_HALLUCINATION: self.rag_hallucination_evaluator,
95
+ }
96
+ return evaluator_map.get(evaluator_type)
97
+
98
+ def correctness_evaluator(self, inputs: dict, outputs: dict, reference_outputs: dict):
99
+ evaluator = create_llm_as_judge(
100
+ prompt=CORRECTNESS_PROMPT,
101
+ feedback_key="correctness",
102
+ model=self.judge_model,
103
+ continuous=True,
104
+ choices=[i/10 for i in range(11)]
105
+ )
106
+ return evaluator(
107
+ inputs=inputs,
108
+ outputs=outputs,
109
+ reference_outputs=reference_outputs
110
+ )
111
+
112
+ def helpfulness_evaluator(self, inputs: dict, outputs: dict):
113
+ evaluator = create_llm_as_judge(
114
+ prompt=RAG_HELPFULNESS_PROMPT,
115
+ feedback_key="helpfulness",
116
+ model=self.judge_model,
117
+ continuous=True,
118
+ choices=[i/10 for i in range(11)]
119
+ )
120
+ return evaluator(
121
+ inputs=inputs,
122
+ outputs=outputs,
123
+ )
124
+
125
+ def conciseness_evaluator(self, inputs: dict, outputs: dict, reference_outputs: dict):
126
+ evaluator = create_llm_as_judge(
127
+ prompt=CONCISENESS_PROMPT,
128
+ feedback_key="conciseness",
129
+ model=self.judge_model,
130
+ continuous=True,
131
+ choices=[i/10 for i in range(11)]
132
+ )
133
+ return evaluator(
134
+ inputs=inputs,
135
+ outputs=outputs,
136
+ reference_outputs=reference_outputs
137
+ )
138
+
139
+ def _find_retrievers(self, run: Run) -> List[Run]:
140
+ retrievers = []
141
+ for child in getattr(run, "child_runs", []):
142
+ if child.run_type == "retriever":
143
+ retrievers.append(child)
144
+ retrievers.extend(self._find_retrievers(child))
145
+ return retrievers
146
+
147
+ def _retriever_documents(self, retrievers_run: List[Run]) -> str:
148
+ unique_contents = set()
149
+ for r in retrievers_run:
150
+ for doc in r.outputs.get("documents", []):
151
+ unique_contents.add(doc.page_content)
152
+ return "\n\n".join(unique_contents)
153
+
154
+ def rag_groundedness_evaluator(self, run: Run):
155
+ evaluator = create_llm_as_judge(
156
+ prompt=RAG_GROUNDEDNESS_PROMPT,
157
+ feedback_key="rag_groundedness",
158
+ model=self.judge_model,
159
+ continuous=True,
160
+ choices=[i/10 for i in range(11)]
161
+ )
162
+ retrievers_run = self._find_retrievers(run)
163
+ if retrievers_run:
164
+ try:
165
+ return evaluator(
166
+ outputs=run.outputs["answer"],
167
+ context=self._retriever_documents(retrievers_run)
168
+ )
169
+ except Exception as e:
170
+ return 0.0
171
+ else:
172
+ return 0.0
173
+
174
+ def rag_hallucination_evaluator(self, inputs: dict, outputs: dict, reference_outputs: dict, run: Run):
175
+ evaluator = create_llm_as_judge(
176
+ prompt=HALLUCINATION_PROMPT,
177
+ feedback_key="rag_hallucination",
178
+ model=self.judge_model,
179
+ continuous=True,
180
+ choices=[i/10 for i in range(11)]
181
+ )
182
+ retrievers_run = self._find_retrievers(run)
183
+ if retrievers_run:
184
+ try:
185
+ return evaluator(
186
+ inputs=inputs['question'],
187
+ outputs=outputs['answer'],
188
+ reference_outputs=reference_outputs['answer'],
189
+ context=self._retriever_documents(retrievers_run)
190
+ )
191
+ except Exception as e:
192
+ return 0.0
193
+ else:
194
+ return 0.0
195
+
196
+ #endregion evaluators
197
+
198
+ #region target
199
+ def _parse_rq(self, inputs: dict, attachments: dict) -> StreamRequest:
200
+ _rq = self.rq.__deepcopy__()
201
+ if not attachments is None and len(attachments) > 0:
202
+ _content = []
203
+ _content.append({"type": "text", "text": inputs["question"]})
204
+ for k,v in attachments.items():
205
+ if isinstance(v, dict):
206
+ _content.append({"type": ("image" if "image" in v.get("mime_type","") else "file"), "url": v.get("presigned_url","")})
207
+ _rq.messages = [LlmMessage(role="user", content=_content)]
208
+ else:
209
+ _rq.messages = [LlmMessage(role="user", content=inputs["question"])]
210
+ return _rq
211
+
212
+ @traceable(run_type="chain",name="stream_internal")
213
+ async def target_internal(self,inputs: dict, attachments: dict) -> dict:
214
+ from ws_bom_robot_app.llm.main import stream
215
+ from unittest.mock import Mock
216
+ from fastapi import Request
217
+ _ctx = Mock(spec=Request)
218
+ _ctx.base_url.return_value = "http://evaluator"
219
+ _rq = self._parse_rq(inputs, attachments)
220
+ _chunks = []
221
+ async for chunk in stream(rq=_rq, ctx=_ctx, formatted=False):
222
+ _chunks.append(chunk)
223
+ _content = ''.join(_chunks) if _chunks else ""
224
+ del _rq, _chunks
225
+ return { "answer": _content.strip() }
226
+
227
+ @traceable(run_type="chain",name="stream_http")
228
+ async def target_http(self,inputs: dict, attachments: dict) -> dict:
229
+ _rq = self._parse_rq(inputs, attachments)
230
+ _host= "http://localhost:6001"
231
+ _endpoint = f"{_host}/api/llm/stream/raw"
232
+ _robot_auth =f"Basic {base64.b64encode((config.robot_user + ':' + config.robot_password).encode('utf-8')).decode('utf-8')}"
233
+ _rs = requests.post(_endpoint, data=_rq.model_dump_json(), stream=True, headers={"Authorization": _robot_auth}, verify=True)
234
+ _content = ''.join([chunk.decode('utf-8') for chunk in _rs.iter_content(chunk_size=1024, decode_unicode=False)])
235
+ del _rq, _rs
236
+ return { "answer": _content.strip() }
237
+ #endregion target
238
+
239
+ async def run(self,
240
+ evaluators: Optional[List[EvaluatorType]] = None,
241
+ target_method: str = "target_internal") -> dict:
242
+ """Run evaluation with specified evaluators
243
+
244
+ Args:
245
+ evaluators: List of evaluator types to use. If None, uses default (correctness only)
246
+ target_method: Method to use for target evaluation ("target_internal" or "target")
247
+
248
+ Returns:
249
+ dict: Evaluation results with scores
250
+
251
+ Usage:
252
+ ```
253
+ await evaluator.run() # Uses default (correctness only)
254
+ await evaluator.run([EvaluatorType.CORRECTNESS, EvaluatorType.HELPFULNESS])
255
+ await evaluator.run(EvaluatorType.all()) # Uses all available evaluators
256
+ ```
257
+ """
258
+ try:
259
+ # evaluator functions
260
+ evaluator_functions = []
261
+ if evaluators is None:
262
+ evaluators = EvaluatorType.default()
263
+ for eval_type in evaluators:
264
+ func = self._get_evaluator_function(eval_type)
265
+ if func:
266
+ evaluator_functions.append(func)
267
+ else:
268
+ print(f"Warning: Unknown evaluator type: {eval_type}")
269
+ if not evaluator_functions:
270
+ print("No valid evaluators provided, using default (correctness)")
271
+ evaluator_functions = [self.correctness_evaluator]
272
+
273
+ # target method
274
+ target_func = getattr(self, target_method, self.target_internal)
275
+
276
+ # run
277
+ _dataset: Dataset = self.data if isinstance(self.data, Dataset) else EvaluatorDataSets.get(self.data[0].dataset_id)
278
+ experiment = await ls_client.aevaluate(
279
+ target_func,
280
+ data=_dataset.name if isinstance(self.data, Dataset) else self.data,
281
+ evaluators=evaluator_functions,
282
+ experiment_prefix=_dataset.name,
283
+ upload_results=True,
284
+ max_concurrency=4,
285
+ metadata={
286
+ "app": _dataset.name,
287
+ "model": f"{self.rq.provider}:{self.rq.model}",
288
+ "judge": self.judge_model,
289
+ "evaluators": [e.value for e in evaluators]
290
+ }
291
+ )
292
+ feedback = list(EvaluatorDataSets.feedback(experiment.experiment_name))
293
+ scores = [f.score for f in feedback]
294
+ url = f"{ls_client._host_url}/o/{ls_client._tenant_id}/datasets/{_dataset.id}/compare?selectedSessions={feedback[0].session_id}"
295
+
296
+ # group scores by evaluator type
297
+ evaluator_scores = {}
298
+ for i, eval_type in enumerate(evaluators):
299
+ eval_scores = [f.score for f in feedback if f.key.lower() == eval_type.value.lower()]
300
+ if eval_scores:
301
+ evaluator_scores[eval_type.value] = sum(eval_scores) / len(eval_scores)
302
+
303
+ return {
304
+ "experiment": {"name": experiment.experiment_name, "url": url},
305
+ "overall_score": sum(scores) / len(scores) if scores else 0,
306
+ "evaluator_scores": evaluator_scores
307
+ }
308
+ except Exception as e:
309
+ from traceback import print_exc
310
+ print(f"Error occurred during evaluation: {e}")
311
+ print_exc()
312
+ return {"error": str(e)}
313
+
314
+ class EvaluatorRunRequest(BaseModel):
315
+ dataset: dict
316
+ rq: StreamRequest
317
+ example: Optional[List[dict]] = None
318
+ evaluators: Optional[List[str]] = None
319
+ judge: Optional[str] = None
@@ -3,7 +3,7 @@ import asyncio, json, logging, os, traceback, re
3
3
  from fastapi import Request
4
4
  from langchain.callbacks.tracers import LangChainTracer
5
5
  from langchain_core.callbacks.base import AsyncCallbackHandler
6
- from langchain_core.messages import AIMessage, HumanMessage
6
+ from langchain_core.messages import BaseMessage, AIMessage, HumanMessage
7
7
  from langsmith import Client as LangSmithClient
8
8
  from typing import AsyncGenerator, List
9
9
  from ws_bom_robot_app.config import config
@@ -14,7 +14,6 @@ from ws_bom_robot_app.llm.models.api import InvokeRequest, StreamRequest
14
14
  from ws_bom_robot_app.llm.providers.llm_manager import LlmInterface
15
15
  from ws_bom_robot_app.llm.tools.tool_builder import get_structured_tools
16
16
  from ws_bom_robot_app.llm.nebuly_handler import NebulyHandler
17
- import ws_bom_robot_app.llm.settings as settings
18
17
 
19
18
  async def invoke(rq: InvokeRequest) -> str:
20
19
  await rq.initialize()
@@ -40,21 +39,30 @@ def _parse_formatted_message(message: str) -> str:
40
39
  except:
41
40
  result = message
42
41
  return result
43
- async def __stream(rq: StreamRequest, ctx: Request, queue: Queue,formatted: bool = True) -> None:
44
- await rq.initialize()
42
+
43
+ async def __stream(rq: StreamRequest, ctx: Request, queue: Queue, formatted: bool = True) -> None:
45
44
  #os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
46
- if formatted:
47
- agent_handler = AgentHandler(queue,rq.provider,rq.thread_id)
48
- else:
49
- agent_handler = RawAgentHandler(queue,rq.provider)
50
- os.environ["AGENT_HANDLER_FORMATTED"] = str(formatted)
51
- callbacks: List[AsyncCallbackHandler] = [agent_handler]
52
- settings.init()
53
45
 
54
- #CREATION OF CHAT HISTORY FOR AGENT
46
+ # rq initialization
47
+ await rq.initialize()
48
+ for tool in rq.app_tools:
49
+ tool.thread_id = rq.thread_id
50
+
51
+ #llm
52
+ __llm: LlmInterface = rq.get_llm()
53
+
54
+ #chat history
55
+ chat_history: list[BaseMessage] = []
55
56
  for message in rq.messages:
56
57
  if message.role in ["human","user"]:
57
- settings.chat_history.append(HumanMessage(content=message.content))
58
+ _content = message.content
59
+ # multimodal content parsing
60
+ if isinstance(_content, list):
61
+ try:
62
+ _content = await __llm.format_multimodal_content(_content)
63
+ except Exception as e:
64
+ logging.warning(f"Error parsing multimodal content {_content[:100]}: {e}")
65
+ chat_history.append(HumanMessage(content=_content))
58
66
  elif message.role in ["ai","assistant"]:
59
67
  message_content = ""
60
68
  if formatted:
@@ -79,37 +87,52 @@ async def __stream(rq: StreamRequest, ctx: Request, queue: Queue,formatted: bool
79
87
  else:
80
88
  message_content = message.content
81
89
  if message_content:
82
- settings.chat_history.append(AIMessage(content=message_content))
90
+ chat_history.append(AIMessage(content=message_content))
91
+
92
+
93
+ #agent handler
94
+ if formatted:
95
+ agent_handler = AgentHandler(queue, rq.provider, rq.thread_id)
96
+ else:
97
+ agent_handler = RawAgentHandler(queue, rq.provider)
98
+ #TODO: move from os.environ to rq
99
+ os.environ["AGENT_HANDLER_FORMATTED"] = str(formatted)
83
100
 
101
+ #callbacks
102
+ ## agent
103
+ callbacks: List[AsyncCallbackHandler] = [agent_handler]
104
+ ## langchain tracing
84
105
  if rq.lang_chain_tracing:
85
106
  client = LangSmithClient(
86
107
  api_key= rq.secrets.get("langChainApiKey", "")
87
108
  )
88
- trace = LangChainTracer(project_name=rq.lang_chain_project,client=client,tags=[str(ctx.base_url)])
109
+ trace = LangChainTracer(project_name=rq.lang_chain_project,client=client,tags=[str(ctx.base_url) if ctx else ''])
89
110
  callbacks.append(trace)
90
-
91
- __llm: LlmInterface =rq.get_llm()
92
- for tool in rq.app_tools:
93
- tool.thread_id = rq.thread_id
94
- processor = AgentLcel(
95
- llm=__llm,
96
- sys_message=rq.system_message,
97
- sys_context=rq.system_context,
98
- tools=get_structured_tools(__llm, tools=rq.app_tools, callbacks=[callbacks], queue=queue),
99
- rules=rq.rules
100
- )
111
+ ## nebuly tracing
101
112
  if rq.secrets.get("nebulyApiKey","") != "":
113
+ user_id = rq.system_context.user.id if rq.system_context and rq.system_context.user and rq.system_context.user.id else None
102
114
  nebuly_callback = NebulyHandler(
103
115
  llm_model=__llm.config.model,
104
116
  threadId=rq.thread_id,
117
+ chat_history=chat_history,
105
118
  url=config.NEBULY_API_URL,
106
119
  api_key=rq.secrets.get("nebulyApiKey", None),
120
+ user_id=user_id
107
121
  )
108
122
  callbacks.append(nebuly_callback)
109
123
 
124
+ # chain
125
+ processor = AgentLcel(
126
+ llm=__llm,
127
+ sys_message=rq.system_message,
128
+ sys_context=rq.system_context,
129
+ tools=get_structured_tools(__llm, tools=rq.app_tools, callbacks=[callbacks], queue=queue),
130
+ rules=rq.rules,
131
+ json_schema=rq.output_structure.get("outputFormat") if rq.output_structure and rq.output_structure.get("outputType") == "json" else None
132
+ )
110
133
  try:
111
134
  await processor.executor.ainvoke(
112
- {"chat_history": settings.chat_history},
135
+ {"chat_history": chat_history},
113
136
  {"callbacks": callbacks},
114
137
  )
115
138
  except Exception as e:
@@ -120,7 +143,7 @@ async def __stream(rq: StreamRequest, ctx: Request, queue: Queue,formatted: bool
120
143
  await queue.put(_error)
121
144
  await queue.put(None)
122
145
 
123
- # Signal the end of streaming
146
+ # signal the end of streaming
124
147
  await queue.put(None)
125
148
 
126
149
  async def stream(rq: StreamRequest, ctx: Request, formatted: bool = True) -> AsyncGenerator[str, None]:
@@ -1,4 +1,4 @@
1
- from typing import List, Dict, Optional, Tuple, Union
1
+ from typing import List, Dict, Optional, Tuple, Union, Any
2
2
  from datetime import datetime
3
3
  from pydantic import AliasChoices, BaseModel, Field, ConfigDict
4
4
  from langchain_core.embeddings import Embeddings
@@ -11,6 +11,39 @@ import os, shutil, uuid
11
11
  from ws_bom_robot_app.config import Settings, config
12
12
 
13
13
  class LlmMessage(BaseModel):
14
+ """
15
+ 💬 multimodal chat
16
+
17
+ The multimodal message allows users to interact with the application using both text and media files.
18
+ `robot` accept multimodal input in a uniform way, regarding the llm provider used.
19
+
20
+ - simple message
21
+
22
+ ```json
23
+ {
24
+ "role": "user",
25
+ "content": "What is the capital of France?"
26
+ }
27
+ ```
28
+
29
+ - multimodal message
30
+
31
+ ```jsonc
32
+ {
33
+ "role": "user",
34
+ "content": [
35
+ { "type": "text", "text": "Read carefully all the attachments, analize the content and provide a summary for each one:" },
36
+ { "type": "image", "url": "https://www.example.com/image/foo.jpg" },
37
+ { "type": "file", "url": "https://www.example.com/pdf/bar.pdf" },
38
+ { "type": "file", "url": "data:plain/text;base64,CiAgICAgIF9fX19fCiAgICAgLyAgIC..." }, // base64 encoded file
39
+ { "type": "media", "mime_type": "plain/text", "data": "CiAgICAgIF9fX19fCiAgICAgLyAgIC..." } // google/gemini specific input format
40
+ ]
41
+ }
42
+ ```
43
+
44
+ > 💡 `url` can be a remote url or a base64 representation of the file: [rfc 2397](https://datatracker.ietf.org/doc/html/rfc2397).
45
+ Can also be used the llm/model specific input format.
46
+ """
14
47
  role: str
15
48
  content: Union[str, list]
16
49
 
@@ -121,6 +154,7 @@ class LlmApp(BaseModel):
121
154
  fine_tuned_model: Optional[str] = Field(None, validation_alias=AliasChoices("fineTunedModel","fine_tuned_model"))
122
155
  lang_chain_tracing: Optional[bool] = Field(False, validation_alias=AliasChoices("langChainTracing","lang_chain_tracing"))
123
156
  lang_chain_project: Optional[str] = Field(None, validation_alias=AliasChoices("langChainProject","lang_chain_project"))
157
+ output_structure: Optional[Dict[str, Any]] = Field(None, validation_alias=AliasChoices("outputStructure","output_structure"))
124
158
  model_config = ConfigDict(
125
159
  extra='allow'
126
160
  )
@@ -130,7 +164,7 @@ class LlmApp(BaseModel):
130
164
  return list(set(
131
165
  os.path.basename(db) for db in [self.vector_db] +
132
166
  ([self.rules.vector_db] if self.rules and self.rules.vector_db else []) +
133
- [db for tool in (self.app_tools or []) for db in [tool.vector_db]]
167
+ [db for tool in (self.app_tools or []) for db in [tool.vector_db] if tool.is_active]
134
168
  if db is not None
135
169
  ))
136
170
  def __decompress_zip(self,zip_file_path, extract_to):
@@ -154,7 +188,7 @@ class LlmApp(BaseModel):
154
188
  for tool in self.app_tools or []:
155
189
  tool.vector_db = os.path.join(_vector_db_folder, os.path.splitext(os.path.basename(tool.vector_db))[0]) if tool.vector_db else None
156
190
  def api_key(self):
157
- return self.secrets.get("openAIApiKey", self.secrets.get("apiKey", ""))
191
+ return self.secrets.get("apiKey", "")
158
192
  def get_llm(self) -> LlmInterface:
159
193
  return LlmManager._list[self.provider](LlmConfig(
160
194
  api_key=self.api_key(),
@@ -169,8 +203,8 @@ class InvokeRequest(LlmApp):
169
203
  mode: str
170
204
 
171
205
  class StreamRequest(LlmApp):
172
- thread_id: Optional[str] = Field(None, validation_alias=AliasChoices("threadId","thread_id"))
173
- msg_id: Optional[str] = Field(None, validation_alias=AliasChoices("msgId","msg_id"))
206
+ thread_id: Optional[str] = Field(default=str(uuid.uuid4()), validation_alias=AliasChoices("threadId","thread_id"))
207
+ msg_id: Optional[str] = Field(default=str(uuid.uuid4()), validation_alias=AliasChoices("msgId","msg_id"))
174
208
  #endregion
175
209
 
176
210
  #region vector_db
@@ -190,7 +224,7 @@ class VectorDbRequest(BaseModel):
190
224
  def config(self) -> Settings:
191
225
  return config
192
226
  def api_key(self):
193
- return self.secrets.get("openAIApiKey", self.secrets.get("apiKey", ""))
227
+ return self.secrets.get("apiKey", "")
194
228
  def out_name(self):
195
229
  if self.vector_db:
196
230
  return ".".join(self.vector_db.split(".")[:-1]) if self.vector_db.endswith(".zip") else self.vector_db
@@ -2,24 +2,23 @@ from typing import Union
2
2
  from ws_bom_robot_app.llm.models.api import NebulyInteraction, NebulyLLMTrace, NebulyRetrievalTrace
3
3
  from datetime import datetime, timezone
4
4
  from langchain_core.callbacks.base import AsyncCallbackHandler
5
- import ws_bom_robot_app.llm.settings as settings
6
5
  from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
7
6
  from langchain_core.outputs import ChatGenerationChunk, GenerationChunk
8
- from uuid import UUID
9
7
 
10
8
  class NebulyHandler(AsyncCallbackHandler):
11
- def __init__(self, llm_model: str | None, threadId: str = None, url: str = None, api_key: str = None):
9
+ def __init__(self, llm_model: str | None, threadId: str = None, chat_history: list[BaseMessage] = [], url: str = None, api_key: str = None, user_id: str | None = None):
12
10
  super().__init__()
13
11
  self.__started: bool = False
14
12
  self.__url: str = url
15
13
  self.__api_key: str = api_key
14
+ self.chat_history = chat_history
16
15
  self.interaction = NebulyInteraction(
17
16
  conversation_id=threadId,
18
17
  input="",
19
18
  output="",
20
19
  time_start="",
21
20
  time_end="",
22
- end_user=threadId,
21
+ end_user= user_id if user_id and user_id != "" else threadId,
23
22
  tags={"model": llm_model},
24
23
  )
25
24
  self.llm_trace = NebulyLLMTrace(
@@ -77,7 +76,7 @@ class NebulyHandler(AsyncCallbackHandler):
77
76
  self.interaction.output = finish.return_values["output"]
78
77
  # Trace
79
78
  self.llm_trace.output = finish.return_values["output"]
80
- message_history = self._convert_to_json_format(settings.chat_history)[:-1]
79
+ message_history = self._convert_to_json_format(self.chat_history)
81
80
  self.llm_trace.messages = self.__parse_multimodal_history(message_history)
82
81
  await self.__send_interaction()
83
82
 
@@ -146,16 +145,20 @@ class NebulyHandler(AsyncCallbackHandler):
146
145
  return payload
147
146
 
148
147
  def __parse_multimodal_input(self, input: list[dict]) -> str:
149
- # Parse the multimodal input and return a string representation
150
- # This is a placeholder implementation, you can customize it as needed
151
- parsed_input = ""
152
- for item in input:
153
- if item.get("type") == "text":
154
- parsed_input += item.get("text", "")
155
- elif item.get("type") == "image_url":
156
- parsed_input += " <image>"
157
- print(parsed_input)
158
- return parsed_input
148
+ """Parse multimodal input and return a string representation."""
149
+ type_mapping = {
150
+ "text": lambda item: item.get("text", ""),
151
+ "image": lambda _: " <image>",
152
+ "image_url": lambda _: " <image>",
153
+ "file": lambda _: " <file>",
154
+ "media": lambda _: " <file>",
155
+ "document": lambda _: " <file>",
156
+ }
157
+
158
+ return "".join(
159
+ type_mapping.get(item.get("type", ""), lambda item: f" <{item.get('type', '')}>")
160
+ (item) for item in input
161
+ )
159
162
 
160
163
  def __parse_multimodal_history(self, messages: list[dict]) -> list[dict]:
161
164
  # Parse the multimodal history and return a list of dictionaries