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
|
@@ -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
|
ws_bom_robot_app/llm/main.py
CHANGED
|
@@ -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
|
-
|
|
44
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
-
#
|
|
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("
|
|
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(
|
|
173
|
-
msg_id: Optional[str] = Field(
|
|
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("
|
|
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(
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|