ws-bom-robot-app 0.0.7__py3-none-any.whl → 0.0.9__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.
@@ -1,15 +1,21 @@
1
- import os
2
- import threading
3
- import schedule, time
1
+ from math import floor
2
+ import schedule, time, threading
4
3
  import asyncio
5
4
  from fastapi import APIRouter
6
5
  from ws_bom_robot_app.task_manager import task_manager
7
6
  from ws_bom_robot_app.llm.utils.kb import kb_cleanup_data_file
8
7
  from ws_bom_robot_app.util import _log
8
+ import random
9
9
 
10
10
  class RecurringJob():
11
+ @staticmethod
12
+ def __add_jitter(interval: int) -> int:
13
+ #delay proportional with interval, min 10, max 100 sec
14
+ jitter: int = max(10, min(100, floor(interval * 0.075)))
15
+ return interval + random.randint(-jitter, jitter)
11
16
  def __init__(self, interval: int, job_func, tags: list[str]):
12
- self.interval = interval
17
+ #add a little jitter by default for better concurrency in case of multiple instances
18
+ self.interval = RecurringJob.__add_jitter(interval)
13
19
  self.job_func = job_func
14
20
  self.is_coroutine = asyncio.iscoroutinefunction(job_func)
15
21
  self.job_func = job_func
@@ -25,8 +31,8 @@ class RecurringJob():
25
31
  class CronManager:
26
32
 
27
33
  _list: dict[str, RecurringJob] = {
28
- 'cleanup-task': RecurringJob(5*60, task_manager.cleanup_task, tags=["cleanup","cleanup-task"]),
29
- 'cleanup-data': RecurringJob(180*60, kb_cleanup_data_file, tags=["cleanup","cleanup-data"]),
34
+ 'cleanup-task': RecurringJob(5*60, task_manager.cleanup_task, tags=["cleanup","cleanup-task"]),
35
+ 'cleanup-data': RecurringJob(180*60, kb_cleanup_data_file, tags=["cleanup","cleanup-data"]),
30
36
  }
31
37
 
32
38
  def __init__(self):
@@ -7,6 +7,7 @@ from langchain_core.messages import AIMessage
7
7
  from langchain_core.runnables import RunnableSerializable
8
8
  from langchain_core.runnables import RunnableLambda
9
9
  from bs4 import BeautifulSoup
10
+ from ws_bom_robot_app.llm.models.api import LlmRules
10
11
  from ws_bom_robot_app.llm.utils.agent_utils import get_rules
11
12
 
12
13
  # SafeDict helper class
@@ -85,19 +86,19 @@ class AgentDescriptor:
85
86
  def get_strategy(name: str) -> AgentDescriptorStrategy:
86
87
  return AgentDescriptor._list.get(name.lower(), DefaultAgentDescriptor())
87
88
 
88
- def __init__(self, api_key: str, prompt: str, mode: str, rules_folder: str = ""):
89
+ def __init__(self, api_key: str, prompt: str, mode: str, rules: LlmRules = None):
89
90
  self.__prompt = prompt
90
91
  self.__llm = ChatOpenAI(model="gpt-4o", temperature=0, api_key=api_key) # type: ignore
91
92
  self.api_key = api_key
92
- self.rules_folder = rules_folder
93
+ self.rules= rules
93
94
  self.strategy = self.get_strategy(mode) # Selects the strategy from the dictionary
94
95
 
95
96
  async def __create_prompt(self, input_dict: dict):
96
97
  input_data = json.loads(input_dict.get("input", {}))
97
98
  system = self.strategy.enrich_prompt(self.__prompt, input_data)
98
- if self.rules_folder:
99
+ if self.rules:
99
100
  rule_input = self.strategy.rule_input(input_data)
100
- rules_prompt = await get_rules(self.rules_folder,self.api_key, rule_input)
101
+ rules_prompt = await get_rules(self.rules,self.api_key, rule_input)
101
102
  system += rules_prompt
102
103
  return ChatPromptTemplate.from_messages(
103
104
  [
@@ -9,20 +9,20 @@ from langchain.agents.output_parsers.openai_tools import OpenAIToolsAgentOutputP
9
9
  from langchain_core.runnables import RunnableLambda
10
10
  from datetime import datetime
11
11
  from langchain_openai import OpenAIEmbeddings
12
- from ws_bom_robot_app.llm.models.api import LlmMessage, LlmAppTool
12
+ from ws_bom_robot_app.llm.models.api import LlmMessage, LlmRules
13
13
  from ws_bom_robot_app.llm.utils.agent_utils import get_rules
14
14
  from ws_bom_robot_app.llm.defaut_prompt import default_prompt
15
15
 
16
16
  class AgentLcel:
17
17
 
18
- def __init__(self, openai_config: dict, sys_message: str, tools: list, rules_folder: str = ""):
18
+ def __init__(self, openai_config: dict, sys_message: str, tools: list, rules: LlmRules = None):
19
19
  self.__apy_key = openai_config["api_key"]
20
20
  self.sys_message = sys_message.format(
21
21
  date_stamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
22
22
  lang="it",
23
23
  )
24
24
  self.__tools = tools
25
- self.rulesPath = rules_folder
25
+ self.rules = rules
26
26
  self.embeddings = OpenAIEmbeddings(api_key= self.__apy_key) # type: ignore
27
27
  self.memory_key = "chat_history"
28
28
  self.__llm = ChatOpenAI(
@@ -37,7 +37,7 @@ class AgentLcel:
37
37
  async def __create_prompt(self, input):
38
38
  message : LlmMessage = input["input"]
39
39
  input = message.content
40
- rules_prompt = await get_rules(self.rulesPath,self.__apy_key, input)
40
+ rules_prompt = await get_rules(self.rules,self.__apy_key, input) if self.rules else ""
41
41
  system = default_prompt + self.sys_message + rules_prompt
42
42
  return ChatPromptTemplate.from_messages(
43
43
  [
@@ -1,13 +1,15 @@
1
1
  import openai
2
- from fastapi import APIRouter, HTTPException
2
+ from typing import Annotated
3
+ from fastapi import APIRouter, HTTPException, Header
3
4
  from fastapi.responses import StreamingResponse
4
5
  from ws_bom_robot_app.llm.agent_description import AgentDescriptor
5
6
  from ws_bom_robot_app.llm.models.api import InvokeRequest, StreamRequest, RulesRequest, KbRequest, VectorDbResponse
6
7
  from ws_bom_robot_app.llm.main import invoke, stream, stream_none
8
+ from ws_bom_robot_app.llm.models.base import IdentifiableEntity
7
9
  from ws_bom_robot_app.llm.vector_store.generator import kb, rules, kb_stream_file
8
10
  from ws_bom_robot_app.llm.tools.tool_manager import ToolManager
9
11
  from ws_bom_robot_app.llm.vector_store.integration.manager import IntegrationManager
10
- from ws_bom_robot_app.task_manager import task_manager
12
+ from ws_bom_robot_app.task_manager import task_manager, TaskHeader
11
13
 
12
14
  router = APIRouter(prefix="/api/llm", tags=["llm"])
13
15
 
@@ -40,12 +42,16 @@ async def _kb(rq: KbRequest) -> VectorDbResponse:
40
42
  return await kb(rq)
41
43
 
42
44
  @router.post("/kb/task")
43
- async def _create_kb_task(rq: KbRequest):
44
- return {"task_id": task_manager.create_task(kb(rq))}
45
+ async def _kb_task(rq: KbRequest, headers: Annotated[TaskHeader, Header()]) -> IdentifiableEntity:
46
+ return task_manager.create_task(kb(rq),headers)
45
47
 
46
48
  @router.post("/rules")
47
- def _rules(rq: RulesRequest) -> VectorDbResponse:
48
- return rules(rq)
49
+ async def _rules(rq: RulesRequest) -> VectorDbResponse:
50
+ return await rules(rq)
51
+
52
+ @router.post("/rules/task")
53
+ async def _rules_task(rq: RulesRequest, headers: Annotated[TaskHeader, Header()]) -> IdentifiableEntity:
54
+ return task_manager.create_task(rules(rq),headers)
49
55
 
50
56
  @router.get("/kb/file/{filename}")
51
57
  async def _kb_get_file(filename: str) -> StreamingResponse:
@@ -17,7 +17,7 @@ async def invoke(rq: InvokeRequest) -> str:
17
17
  processor = AgentDescriptor(api_key=rq.secrets["openAIApiKey"],
18
18
  prompt=rq.system_message,
19
19
  mode = rq.mode,
20
- rules_folder=rq.rules_vector_db if rq.rules_vector_db else ""
20
+ rules=rq.rules if rq.rules else None
21
21
  )
22
22
  result: AIMessage = await processor.run_agent(_msg)
23
23
  return {"result": result.content}
@@ -49,7 +49,7 @@ async def __stream(rq: StreamRequest,formatted: bool = True) -> None:
49
49
  openai_config={"api_key": rq.secrets["openAIApiKey"], "openai_model": rq.model, "temperature": rq.temperature},
50
50
  sys_message=rq.system_message,
51
51
  tools=get_structured_tools(tools=rq.app_tools, api_key=rq.secrets["openAIApiKey"], callbacks=[callbacks[0]]),
52
- rules_folder=rq.rules_vector_db if rq.rules_vector_db else ""
52
+ rules=rq.rules
53
53
  )
54
54
 
55
55
  if "NEBULY_API_KEY" in os.environ:
@@ -15,6 +15,10 @@ class LlmSearchSettings(BaseModel):
15
15
  score_threshold_id: Optional[float] = Field(None, validation_alias=AliasChoices("scoreThresholdId","score_threshold_id"))
16
16
  search_k: Optional[int] = Field(None, validation_alias=AliasChoices("searchK","search_k"))
17
17
 
18
+ class LlmRules(BaseModel):
19
+ vector_db: Optional[str] = Field(None, validation_alias=AliasChoices("rulesVectorDb","vector_db"))
20
+ threshold: Optional[float] = 0.7
21
+
18
22
  class LlmAppToolChainSettings(BaseModel):
19
23
  prompt: Optional[str] = None
20
24
  model: Optional[str] = None
@@ -41,7 +45,6 @@ class LlmAppTool(BaseModel):
41
45
  endpoints: Optional[List[LlmKbEndpoint]] = Field(None, validation_alias=AliasChoices("externalEndpoints","endpoints"))
42
46
  waiting_message: Optional[str] = Field("", validation_alias=AliasChoices("waitingMessage","waiting_message"))
43
47
  vector_db: Optional[str] = Field(None, validation_alias=AliasChoices("vectorDbFile","vector_db"))
44
- rules_vector_db: Optional[str] = Field(None, validation_alias=AliasChoices("rulesVectorDb","rules_vector_db"))
45
48
  is_active: Optional[bool] = Field(True, validation_alias=AliasChoices("isActive","is_active"))
46
49
  model_config = ConfigDict(
47
50
  extra='allow'
@@ -58,7 +61,7 @@ class LlmApp(BaseModel):
58
61
  secrets: Dict[str, str]
59
62
  app_tools: Optional[List[LlmAppTool]] = Field([], validation_alias=AliasChoices("appTools","app_tools"))
60
63
  vector_db: Optional[str] = Field(None, validation_alias=AliasChoices("vectorDb","vector_db"))
61
- rules_vector_db: Optional[str] = Field(None, validation_alias=AliasChoices("rulesVectorDb","rules_vector_db"))
64
+ rules: Optional[LlmRules] = None
62
65
  fine_tuned_model: Optional[str] = Field(None, validation_alias=AliasChoices("fineTunedModel","fine_tuned_model"))
63
66
  lang_chain_tracing: Optional[bool] = Field(False, validation_alias=AliasChoices("langChainTracing","lang_chain_tracing"))
64
67
  lang_chain_project: Optional[str] = Field(None, validation_alias=AliasChoices("langChainProject","lang_chain_project"))
@@ -68,10 +71,12 @@ class LlmApp(BaseModel):
68
71
  def __vector_db_folder(self) -> str:
69
72
  return os.path.join(config.robot_data_folder,config.robot_data_db_folder,config.robot_data_db_folder_store)
70
73
  def __vector_dbs(self):
71
- return list(set(
72
- [os.path.basename(db) for db in [self.vector_db, self.rules_vector_db] +
73
- [db for tool in (self.app_tools or []) for db in [tool.vector_db,tool.rules_vector_db]]
74
- if db is not None]))
74
+ return list(set(
75
+ os.path.basename(db) for db in [self.vector_db] +
76
+ ([self.rules.vector_db] if self.rules and self.rules.vector_db else []) +
77
+ [db for tool in (self.app_tools or []) for db in [tool.vector_db]]
78
+ if db is not None
79
+ ))
75
80
  def __decompress_zip(self,zip_file_path, extract_to):
76
81
  shutil.unpack_archive(zip_file_path, extract_to, "zip")
77
82
  os.remove(zip_file_path)
@@ -96,7 +101,8 @@ class LlmApp(BaseModel):
96
101
  def __normalize_vector_db_path(self) -> None:
97
102
  _vector_db_folder = self.__vector_db_folder()
98
103
  self.vector_db = os.path.join(_vector_db_folder, os.path.splitext(os.path.basename(self.vector_db))[0]) if self.vector_db else None
99
- self.rules_vector_db = os.path.join(_vector_db_folder, os.path.splitext(os.path.basename(self.rules_vector_db))[0]) if self.rules_vector_db else None
104
+ if self.rules:
105
+ self.rules.vector_db = os.path.join(_vector_db_folder, os.path.splitext(os.path.basename(self.rules.vector_db))[0]) if self.rules.vector_db else ""
100
106
  for tool in self.app_tools or []:
101
107
  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
102
108
  async def initialize(self) -> None:
@@ -0,0 +1,6 @@
1
+ from typing import Union
2
+ from uuid import UUID
3
+ from pydantic import BaseModel
4
+
5
+ class IdentifiableEntity(BaseModel):
6
+ id: Union[str, int, UUID]
@@ -129,8 +129,8 @@ async def load_endpoints(endpoints: list[LlmKbEndpoint], destination_directory:
129
129
  }
130
130
  headers['Authorization'] = auth_formats[endpoint.authentication](endpoint.auth_secret)
131
131
  try:
132
- async with httpx.AsyncClient() as client:
133
- response = await client.get(endpoint.endpoint_url, headers=headers)
132
+ async with httpx.AsyncClient(headers=headers,timeout=60) as client:
133
+ response = await client.get(endpoint.endpoint_url)
134
134
  response.raise_for_status()
135
135
 
136
136
  mime_type = response.headers.get('content-type', None)
@@ -1,13 +1,14 @@
1
1
  import os
2
+ from ws_bom_robot_app.llm.models.api import LlmRules
2
3
  from ws_bom_robot_app.llm.utils.print import HiddenPrints
3
4
  from ws_bom_robot_app.llm.utils.faiss_helper import FaissHelper
4
5
 
5
- async def get_rules(rules_folder: str, api_key:str, input: str) -> str:
6
+ async def get_rules(rules: LlmRules, api_key:str, input: str) -> str:
6
7
  with HiddenPrints():
7
- if input=="" or rules_folder == "" or not os.path.exists(rules_folder):
8
+ if any([input=="",rules is None,rules.vector_db == "",not os.path.exists(rules.vector_db)]):
8
9
  return ""
9
10
  rules_prompt = ""
10
- rules_doc = await FaissHelper.invoke(rules_folder,api_key,input,search_type="similarity_score_threshold", search_kwargs={"score_threshold": 0.7}) #type: ignore
11
+ rules_doc = await FaissHelper.invoke(rules.vector_db,api_key,input,search_type="similarity_score_threshold", search_kwargs={"score_threshold": rules.threshold}) #type: ignore
11
12
  if len(rules_doc) > 0:
12
13
  rules_prompt = "\nFollow this rules: \n RULES: \n"
13
14
  for rule_doc in rules_doc:
@@ -0,0 +1,53 @@
1
+ import asyncio
2
+ import base64
3
+ from datetime import datetime, timezone
4
+ from math import floor
5
+ import httpx
6
+ import logging
7
+ from standardwebhooks import Webhook
8
+ from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
9
+ from typing import Optional
10
+ from ws_bom_robot_app.config import config
11
+ from ws_bom_robot_app.llm.models.base import IdentifiableEntity
12
+
13
+ class WebhookNotifier:
14
+ MAX_RETRIES = 10
15
+ BASE_DELAY = 5 # seconds
16
+ MAX_DELAY = 120 # maximum delay between retries
17
+
18
+ @retry(
19
+ stop=stop_after_attempt(MAX_RETRIES),
20
+ wait=wait_exponential(multiplier=BASE_DELAY, max=MAX_DELAY),
21
+ retry=retry_if_exception_type((httpx.HTTPError, asyncio.TimeoutError)),
22
+ after=lambda retry_state: logging.warning(
23
+ f"Webhook notification attempt {retry_state.attempt_number} failed "
24
+ f"for endpoint {retry_state.args[1]}"
25
+ )
26
+ )
27
+ async def notify_webhook(
28
+ self,
29
+ data: IdentifiableEntity,
30
+ endpoint: str,
31
+ timeout: Optional[float] = 30.0
32
+ ) -> None:
33
+ """
34
+ Notify a signed webhook. App credentials are used to sign the webhook.
35
+ Args:
36
+ task: The task result to send
37
+ endpoint: Webhook URL to notify
38
+ timeout: Request timeout in seconds
39
+ Raises:
40
+ Exception: If all retry attempts fail
41
+ """
42
+ wh = Webhook(base64.b64encode((config.robot_user + ':' + config.robot_password).encode('utf-8')).decode('utf-8'))
43
+ _data = data.model_dump_json()
44
+ _datetime = datetime.now(tz=timezone.utc)
45
+ _signature = wh.sign(data.id,_datetime,_data)
46
+ _headers = {
47
+ 'webhook-id': data.id,
48
+ 'webhook-timestamp': str(floor(_datetime.replace(tzinfo=timezone.utc).timestamp())),
49
+ 'webhook-signature': _signature
50
+ }
51
+ async with httpx.AsyncClient(headers=_headers,verify=False,timeout=timeout) as client:
52
+ response = await client.post(endpoint, data=_data)
53
+ response.raise_for_status()
@@ -2,9 +2,7 @@ import os, shutil, traceback
2
2
  import asyncio
3
3
  from fastapi import HTTPException
4
4
  from fastapi.responses import StreamingResponse
5
- from langchain_openai import OpenAIEmbeddings
6
5
  from langchain_core.documents import Document
7
- from ws_bom_robot_app.llm import api
8
6
  from ws_bom_robot_app.llm.vector_store.loader.base import Loader
9
7
  from ws_bom_robot_app.llm.models.api import RulesRequest, KbRequest, VectorDbResponse
10
8
  from ws_bom_robot_app.llm.vector_store.integration.manager import IntegrationManager
@@ -23,7 +21,8 @@ async def rules(rq: RulesRequest) -> VectorDbResponse:
23
21
  db_file_path = shutil.make_archive(os.path.join(_config.robot_data_folder, _config.robot_data_db_folder, _config.robot_data_db_folder_out, db_name), "zip", store_path)
24
22
  return VectorDbResponse(file = os.path.basename(db_file_path))
25
23
  except Exception as e:
26
- os.removedirs(store_path)
24
+ if os.path.exists(store_path):
25
+ shutil.rmtree(store_path)
27
26
  return VectorDbResponse(success = False, error = str(e))
28
27
 
29
28
  #@atimer
ws_bom_robot_app/main.py CHANGED
@@ -33,6 +33,7 @@ async def openapi(authenticate: bool = Depends(authenticate)):
33
33
 
34
34
  @app.get("/diag",tags=["diag"])
35
35
  def diag(authenticate: bool = Depends(authenticate)):
36
+ import pkg_resources
36
37
  from ws_bom_robot_app.llm.vector_store.loader.base import Loader as wsll
37
38
  from ws_bom_robot_app.llm.vector_store.integration.manager import IntegrationManager as wsim
38
39
  from ws_bom_robot_app.llm.tools.tool_manager import ToolManager as wstm
@@ -58,6 +59,7 @@ def diag(authenticate: bool = Depends(authenticate)):
58
59
  "os": {
59
60
  "pid": os.getpid(),
60
61
  "cwd": os.getcwd(),
62
+ "ws_bom_robot_app": pkg_resources.get_distribution("ws_bom_robot_app").version,
61
63
  "env": os.environ,
62
64
  },
63
65
  },
@@ -1,14 +1,24 @@
1
+ import inspect
1
2
  import asyncio, os
2
3
  from datetime import datetime, timedelta
3
4
  from enum import Enum
4
- from typing import TypeVar, Optional, Dict, Union
5
- from pydantic import BaseModel, ConfigDict, computed_field
5
+ from typing import Annotated, TypeVar, Optional, Dict, Union, Any
6
+ from pydantic import BaseModel, ConfigDict, Field, computed_field
6
7
  from uuid import uuid4
7
8
  from fastapi import APIRouter, HTTPException
8
9
  from ws_bom_robot_app.config import config
10
+ from ws_bom_robot_app.llm.models.base import IdentifiableEntity
11
+ from ws_bom_robot_app.llm.utils.webhooks import WebhookNotifier
9
12
 
10
13
  T = TypeVar('T')
11
14
 
15
+ class TaskHeader(BaseModel):
16
+ x_ws_bom_msg_type: Optional[str] = None
17
+ x_ws_bom_webhooks: Optional[str] = None
18
+ model_config = ConfigDict(
19
+ extra='allow'
20
+ )
21
+
12
22
  class TaskMetaData(BaseModel):
13
23
  start_time: datetime
14
24
  end_time: Optional[datetime] = None
@@ -25,38 +35,52 @@ class TaskMetaData(BaseModel):
25
35
  }
26
36
  )
27
37
 
28
- class TaskStatus(BaseModel):
38
+ class TaskStatus(IdentifiableEntity):
29
39
  class TaskStatusEnum(str, Enum):
30
40
  pending = "pending"
31
41
  completed = "completed"
32
42
  failure = "failure"
33
- task_id: str
43
+ type: Optional[str] = None
34
44
  status: TaskStatusEnum
35
- #result: Optional[Dict[str, Any]] = None
36
45
  result: Optional[T] = None
37
46
  metadata: TaskMetaData = None
38
47
  error: Optional[str] = None
39
48
 
40
- class TaskEntry(BaseModel):
41
- id: str
42
- task: Union[asyncio.Task, None] = None
43
- status: TaskStatus = None
44
- def __init__(self, **kwargs):
45
- super().__init__(**kwargs)
46
- self.status = TaskStatus(
47
- task_id=self.id,
48
- status=TaskStatus.TaskStatusEnum.pending,
49
- metadata=TaskMetaData(start_time=datetime.now(), source=str(self.task), pid=os.getpid())
50
- )
49
+ class TaskEntry(IdentifiableEntity):
50
+ task: Annotated[asyncio.Task, Field(default=None, validate_default=False)] = None
51
+ headers: TaskHeader | None = None
52
+ status: Union[TaskStatus, None] = None
53
+ def _get_coroutine_name(self, coroutine: asyncio.coroutines) -> str:
54
+ if inspect.iscoroutine(coroutine):
55
+ return coroutine.cr_code.co_name
56
+ return "<unknown>"
57
+ def __init__(self, **data):
58
+ #separate task from data to handle asyncio.Task
59
+ task = data.pop('task',None)
60
+ super().__init__(**data)
61
+ #bypass pydantic validation
62
+ object.__setattr__(self, 'task', task)
63
+ #init status
64
+ if not self.status:
65
+ self.status = TaskStatus(
66
+ id=self.id,
67
+ type=self.headers.x_ws_bom_msg_type if self.headers and self.headers.x_ws_bom_msg_type else self._get_coroutine_name(task._coro) if task else None,
68
+ status=TaskStatus.TaskStatusEnum.pending,
69
+ metadata=TaskMetaData(
70
+ start_time=datetime.now(),
71
+ source=self._get_coroutine_name(task._coro) if task else None,
72
+ pid=os.getpid())
73
+ )
51
74
  model_config = ConfigDict(
52
- arbitrary_types_allowed=True
75
+ arbitrary_types_allowed=True,
76
+ validate_assignment=True
53
77
  )
54
78
 
55
79
  class TaskManager:
56
80
  def __init__(self):
57
81
  self.tasks: Dict[str, TaskEntry] = {}
58
82
 
59
- def _task_done_callback(self, task_id: str):
83
+ def _task_done_callback(self, task_id: str, headers: TaskHeader | None = None):
60
84
  def callback(task: asyncio.Task):
61
85
  if _task := self.tasks.get(task_id):
62
86
  try:
@@ -68,14 +92,21 @@ class TaskManager:
68
92
  _task.status.error = str(e)
69
93
  finally:
70
94
  _task.status.metadata.end_time = datetime.now()
95
+ if headers and headers.x_ws_bom_webhooks:
96
+ asyncio.create_task(
97
+ WebhookNotifier().notify_webhook(_task.status,headers.x_ws_bom_webhooks)
98
+ )
71
99
  return callback
72
100
 
73
- def create_task(self, coroutine: asyncio.coroutines) -> str:
101
+ def create_task(self, coroutine: asyncio.coroutines, headers: TaskHeader | None = None) -> IdentifiableEntity:
74
102
  _task = asyncio.create_task(coroutine)
75
- task = TaskEntry(id=str(uuid4()),task=_task)
76
- task.task.add_done_callback(self._task_done_callback(task.id))
103
+ task = TaskEntry(
104
+ id=str(uuid4()),
105
+ task=_task,
106
+ headers=headers)
107
+ task.task.add_done_callback(self._task_done_callback(task.id, headers))
77
108
  self.tasks[task.id] = task
78
- return task.id
109
+ return IdentifiableEntity(id=task.id)
79
110
 
80
111
  def get_task(self, task_id: str) -> TaskEntry | None:
81
112
  if _task := self.tasks.get(task_id):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ws_bom_robot_app
3
- Version: 0.0.7
3
+ Version: 0.0.9
4
4
  Summary: A FastAPI application serving ws bom/robot/llm platform ai.
5
5
  Home-page: https://github.com/websolutespa/bom
6
6
  Author: Websolute Spa
@@ -10,23 +10,24 @@ Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Operating System :: OS Independent
11
11
  Requires-Python: >=3.12
12
12
  Description-Content-Type: text/markdown
13
- Requires-Dist: schedule
14
- Requires-Dist: aiofiles
13
+ Requires-Dist: standardwebhooks==1.0.0
14
+ Requires-Dist: schedule==1.2.2
15
+ Requires-Dist: aiofiles==24.1.0
15
16
  Requires-Dist: pydantic==2.9.2
16
17
  Requires-Dist: pydantic-settings==2.6.0
17
18
  Requires-Dist: fastapi[standard]==0.115.5
18
- Requires-Dist: langchain>=0.3.0
19
- Requires-Dist: langchain-openai>=0.2.0
20
- Requires-Dist: langchain-community>=0.3.0
21
- Requires-Dist: langchain-core>=0.3.1
19
+ Requires-Dist: langchain==0.3.9
20
+ Requires-Dist: langchain-openai==0.2.10
21
+ Requires-Dist: langchain-community==0.3.8
22
+ Requires-Dist: langchain-core==0.3.21
22
23
  Requires-Dist: faiss-cpu==1.9.0
23
- Requires-Dist: python-magic
24
+ Requires-Dist: python-magic==0.4.27
24
25
  Requires-Dist: opencv-python-headless==4.10.0.84
25
26
  Requires-Dist: jmespath==1.0.1
26
27
  Requires-Dist: unstructured[all-docs]==0.15.14
27
28
  Requires-Dist: langchain_unstructured==0.1.5
28
29
  Requires-Dist: html5lib==1.1
29
- Requires-Dist: markdownify>=0.13.1
30
+ Requires-Dist: markdownify==0.14.1
30
31
  Requires-Dist: nebuly==0.3.33
31
32
 
32
33
  # 🤖 ws-bom-robot-app
@@ -1,21 +1,22 @@
1
1
  ws_bom_robot_app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  ws_bom_robot_app/auth.py,sha256=84nIbmJsMrNs0sxIQGEHbjsjc2P6ZrZZGSn8dkiL6is,895
3
3
  ws_bom_robot_app/config.py,sha256=VUoE6MBpYnZ8FQvUjWOLCzfs35aUvpwrz4ROFVbO3AA,1831
4
- ws_bom_robot_app/cron_manager.py,sha256=p0l0ybwfNhPn-R0kr3sq4rmR9VW3XDrsOu1BgvapYKc,3163
5
- ws_bom_robot_app/main.py,sha256=OZ0s2L2fKEwH46Vnt5KGEbuq-udEJH4WsinR2Ws8B4E,3433
6
- ws_bom_robot_app/task_manager.py,sha256=O6ZiQto0SxqZWja8bVc-Sok5roT_ja-uOISDYs0oBGE,4284
4
+ ws_bom_robot_app/cron_manager.py,sha256=Gd76VwhvlauZ5jCrZa4gyM1NoQdynzskfRYPw6niFyk,3548
5
+ ws_bom_robot_app/main.py,sha256=UPRqJkdhFBS8vmJYFJb4hgpx4pMFwOCrbJwt5MTQymE,3556
6
+ ws_bom_robot_app/task_manager.py,sha256=aiQV5EJ_Q0B0mTcERVbzgaEepEUf4OtljC_UACs6pr8,5837
7
7
  ws_bom_robot_app/util.py,sha256=3aBK-bhsvKJwJeWOHh0c1B1BOyJ_tnUxOa1mJmFKwYQ,2618
8
8
  ws_bom_robot_app/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- ws_bom_robot_app/llm/agent_description.py,sha256=9gK7MfsPvk7YTvk-bbLU_r4ygoEUUrepKxuj9zLxsgI,4681
9
+ ws_bom_robot_app/llm/agent_description.py,sha256=SDJYMmwfdMxEK3a_HDEQ19bfNKmwMSFf5hqU0VSCCIE,4705
10
10
  ws_bom_robot_app/llm/agent_handler.py,sha256=hbOf9i-ynDL3bcClqtUG-yWY8ohbCxONfT5ek9Cv0gY,5667
11
- ws_bom_robot_app/llm/agent_lcel.py,sha256=FzRt_u-zC78-DLS9cigO2oA6bsX6SOERuAFbevSXpT8,2701
12
- ws_bom_robot_app/llm/api.py,sha256=A1IFiiBLHdCJsFDllWTAdQEgkRm5a0BjU-oKozxlhiw,2880
11
+ ws_bom_robot_app/llm/agent_lcel.py,sha256=mOh9jfqAuN8XGNqreAXt4xw_PMydqlglIcXfkw3ppBk,2706
12
+ ws_bom_robot_app/llm/api.py,sha256=5cO49yhU5EXvl20zJORmrZZPc1G_nlvftdcF7cyn4Qc,3252
13
13
  ws_bom_robot_app/llm/defaut_prompt.py,sha256=pn5a4lNLWE1NngHYjA_7tD8GasePMgsgude5fIJxsW0,756
14
- ws_bom_robot_app/llm/main.py,sha256=Q3A_Vgz09fhj78_PNVpDL9VQCYQd2lW_hmfyEKa_17Q,3667
14
+ ws_bom_robot_app/llm/main.py,sha256=HWTaAmxZpVo9HEc2cfv9DqA3tlaVGl1NdBONgEI16ds,3595
15
15
  ws_bom_robot_app/llm/settings.py,sha256=EkFGCppORenStH9W4e6_dYvQ-5p6xiEMpmUHBqNqG9M,117
16
16
  ws_bom_robot_app/llm/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- ws_bom_robot_app/llm/models/api.py,sha256=Bi9YwNBJExoZzPVCerAoweWCY9zGAF9rn_N_aBNKmrU,6922
18
- ws_bom_robot_app/llm/models/kb.py,sha256=EqC9M_nQzDKX9ttL5QG2d04aH19sh-svJdw1MinvGfk,8925
17
+ ws_bom_robot_app/llm/models/api.py,sha256=IGeHJhyMaZ5iqOPNj6Bko2jp6R9yALMfpyeOzwW_NGk,6982
18
+ ws_bom_robot_app/llm/models/base.py,sha256=1TqxuTK3rjJEALn7lvgoen_1ba3R2brAgGx6EDTtDZo,152
19
+ ws_bom_robot_app/llm/models/kb.py,sha256=wsvIb1aaZO0M-04VsKY-VuplHSHyft6UgYOPEjjDAnc,8934
19
20
  ws_bom_robot_app/llm/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
21
  ws_bom_robot_app/llm/tools/tool_builder.py,sha256=rkYu0PrXV84PMi7INjCSWlrWMykUCI8aeF-QjZgLysM,854
21
22
  ws_bom_robot_app/llm/tools/tool_manager.py,sha256=y4K1NiDsVbdZjk2xUEK_T6j-4fHmV5QY02j5tHcoBRs,3708
@@ -23,13 +24,14 @@ ws_bom_robot_app/llm/tools/utils.py,sha256=Cx_6L1IpJCEQ9j0zd1Z3iou5li9mnuZbVilOY
23
24
  ws_bom_robot_app/llm/tools/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
25
  ws_bom_robot_app/llm/tools/models/main.py,sha256=LsOJ7vkcSzYLoE1oa3TG0Rs0pr9J5VS_e4li6aDx_fw,260
25
26
  ws_bom_robot_app/llm/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- ws_bom_robot_app/llm/utils/agent_utils.py,sha256=vdX_1Kxn6RcBljOSBlee1avi_0TyIk-rm0RGGeLt468,707
27
+ ws_bom_robot_app/llm/utils/agent_utils.py,sha256=OdrFcu2WAUFfsxj7moEehaYrUp-PX82KGIHgaWetXQ4,794
27
28
  ws_bom_robot_app/llm/utils/download.py,sha256=iAUxH_NiCpTPtGzhC4hBtxotd2HPFt2MBhttslIxqiI,3194
28
29
  ws_bom_robot_app/llm/utils/faiss_helper.py,sha256=DowmroVT6eIbvnA-TG84PS_D7ujvxSRIKdLuIcJmd6Q,4650
29
30
  ws_bom_robot_app/llm/utils/kb.py,sha256=jja45WCbNI7SGEgqDS99nErlwB5eY8Ga7BMnhdMHZ90,1279
30
31
  ws_bom_robot_app/llm/utils/print.py,sha256=bpLWY0KHXe7x7PWcWy8NS54ZWzHY8b4jrLRkpnDl108,818
32
+ ws_bom_robot_app/llm/utils/webhooks.py,sha256=Sdwu4dkbUC3JEf9ck1hsu4gSGQdj7gcsIrhs3cOVXxk,2112
31
33
  ws_bom_robot_app/llm/vector_store/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
- ws_bom_robot_app/llm/vector_store/generator.py,sha256=_39HONBDSnIVOECoxyVHStg2XX4-m9Enm4pECm7wRBI,5816
34
+ ws_bom_robot_app/llm/vector_store/generator.py,sha256=DM1jQB6nb03GYFMaO-qN6IX29apFZOYkxEKt6wrXdIQ,5770
33
35
  ws_bom_robot_app/llm/vector_store/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
36
  ws_bom_robot_app/llm/vector_store/integration/base.py,sha256=eCKD3U0KPoVDMtKr2iZqauMFEKd9b2k6rqPG_YjDy0g,626
35
37
  ws_bom_robot_app/llm/vector_store/integration/manager.py,sha256=cSFlE2erMv3Uchy788mlCFdcvmyeoqdeIiGmJ9QbLhY,583
@@ -37,7 +39,7 @@ ws_bom_robot_app/llm/vector_store/integration/sitemap.py,sha256=bdrgHBVB8Jt3xiRu
37
39
  ws_bom_robot_app/llm/vector_store/loader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
40
  ws_bom_robot_app/llm/vector_store/loader/base.py,sha256=YKtJWTVQuOvRR2CLYTa0bGEbjqgmYVVud1YKQ9QLibc,4536
39
41
  ws_bom_robot_app/llm/vector_store/loader/json_loader.py,sha256=G9BoxwsevgqL72h2n28O2LpzaCYNymBkX66wru9GkCw,884
40
- ws_bom_robot_app-0.0.7.dist-info/METADATA,sha256=6CJ3Dxk0F_0N00bVKmu79zTkeP6n5EnyHkCUdBs0FKw,5608
41
- ws_bom_robot_app-0.0.7.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
42
- ws_bom_robot_app-0.0.7.dist-info/top_level.txt,sha256=Yl0akyHVbynsBX_N7wx3H3ZTkcMLjYyLJs5zBMDAKcM,17
43
- ws_bom_robot_app-0.0.7.dist-info/RECORD,,
42
+ ws_bom_robot_app-0.0.9.dist-info/METADATA,sha256=oTNkmsn9EzxoJet-nnJDCxWDpVFeJuMzHp5fVL8c2SY,5673
43
+ ws_bom_robot_app-0.0.9.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
44
+ ws_bom_robot_app-0.0.9.dist-info/top_level.txt,sha256=Yl0akyHVbynsBX_N7wx3H3ZTkcMLjYyLJs5zBMDAKcM,17
45
+ ws_bom_robot_app-0.0.9.dist-info/RECORD,,