ws-bom-robot-app 0.0.83__tar.gz → 0.0.85__tar.gz

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 (83) hide show
  1. {ws_bom_robot_app-0.0.83/ws_bom_robot_app.egg-info → ws_bom_robot_app-0.0.85}/PKG-INFO +1 -1
  2. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/setup.py +1 -1
  3. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/config.py +2 -0
  4. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/cron_manager.py +3 -3
  5. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/utils/chunker.py +6 -1
  6. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/utils/cleanup.py +1 -1
  7. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/db/base.py +2 -2
  8. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/loader/base.py +35 -0
  9. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/loader/docling.py +31 -21
  10. ws_bom_robot_app-0.0.85/ws_bom_robot_app/main.py +156 -0
  11. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/subprocess_runner.py +3 -0
  12. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/task_manager.py +79 -60
  13. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/util.py +6 -0
  14. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85/ws_bom_robot_app.egg-info}/PKG-INFO +1 -1
  15. ws_bom_robot_app-0.0.83/ws_bom_robot_app/main.py +0 -157
  16. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/MANIFEST.in +0 -0
  17. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/README.md +0 -0
  18. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/pyproject.toml +0 -0
  19. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/requirements.txt +0 -0
  20. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/setup.cfg +0 -0
  21. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/__init__.py +0 -0
  22. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/auth.py +0 -0
  23. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/__init__.py +0 -0
  24. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/agent_context.py +0 -0
  25. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/agent_description.py +0 -0
  26. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/agent_handler.py +0 -0
  27. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/agent_lcel.py +0 -0
  28. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/api.py +0 -0
  29. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/defaut_prompt.py +0 -0
  30. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/feedbacks/__init__.py +0 -0
  31. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/feedbacks/feedback_manager.py +0 -0
  32. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/main.py +0 -0
  33. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/models/__init__.py +0 -0
  34. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/models/api.py +0 -0
  35. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/models/base.py +0 -0
  36. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/models/feedback.py +0 -0
  37. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/models/kb.py +0 -0
  38. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/nebuly_handler.py +0 -0
  39. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/providers/__init__.py +0 -0
  40. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/providers/llm_manager.py +0 -0
  41. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/tools/__init__.py +0 -0
  42. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/tools/models/__init__.py +0 -0
  43. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/tools/models/main.py +0 -0
  44. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/tools/tool_builder.py +0 -0
  45. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/tools/tool_manager.py +0 -0
  46. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/tools/utils.py +0 -0
  47. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/utils/__init__.py +0 -0
  48. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/utils/agent.py +0 -0
  49. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/utils/cms.py +0 -0
  50. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/utils/download.py +0 -0
  51. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/utils/print.py +0 -0
  52. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/utils/secrets.py +0 -0
  53. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/utils/webhooks.py +0 -0
  54. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/__init__.py +0 -0
  55. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/db/__init__.py +0 -0
  56. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/db/chroma.py +0 -0
  57. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/db/faiss.py +0 -0
  58. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/db/manager.py +0 -0
  59. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/db/qdrant.py +0 -0
  60. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/generator.py +0 -0
  61. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/integration/__init__.py +0 -0
  62. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/integration/azure.py +0 -0
  63. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/integration/base.py +0 -0
  64. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/integration/confluence.py +0 -0
  65. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/integration/dropbox.py +0 -0
  66. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/integration/gcs.py +0 -0
  67. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/integration/github.py +0 -0
  68. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/integration/googledrive.py +0 -0
  69. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/integration/jira.py +0 -0
  70. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/integration/manager.py +0 -0
  71. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/integration/s3.py +0 -0
  72. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/integration/sftp.py +0 -0
  73. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/integration/sharepoint.py +0 -0
  74. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/integration/shopify.py +0 -0
  75. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/integration/sitemap.py +0 -0
  76. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/integration/slack.py +0 -0
  77. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/integration/thron.py +0 -0
  78. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/loader/__init__.py +0 -0
  79. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app/llm/vector_store/loader/json_loader.py +0 -0
  80. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app.egg-info/SOURCES.txt +0 -0
  81. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app.egg-info/dependency_links.txt +0 -0
  82. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app.egg-info/requires.txt +0 -0
  83. {ws_bom_robot_app-0.0.83 → ws_bom_robot_app-0.0.85}/ws_bom_robot_app.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ws_bom_robot_app
3
- Version: 0.0.83
3
+ Version: 0.0.85
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
@@ -4,7 +4,7 @@ _requirements = [line.split('#')[0].strip() for line in open("requirements.txt")
4
4
 
5
5
  setup(
6
6
  name="ws_bom_robot_app",
7
- version="0.0.83",
7
+ version="0.0.85",
8
8
  description="A FastAPI application serving ws bom/robot/llm platform ai.",
9
9
  long_description=open("README.md", encoding='utf-8').read(),
10
10
  long_description_content_type="text/markdown",
@@ -23,6 +23,8 @@ class Settings(BaseSettings):
23
23
  robot_task_strategy: str = 'memory' # memory / db
24
24
  robot_task_mp_enable: bool = True
25
25
  robot_task_mp_method: str = 'spawn' # spawn / fork
26
+ robot_task_mp_max_retries: int = 1
27
+ robot_task_mp_retry_delay: float = 60 # seconds
26
28
  robot_cron_strategy: str = 'memory' # memory / db
27
29
  robot_cms_host: str = ''
28
30
  robot_cms_auth: str = ''
@@ -56,9 +56,9 @@ class Job:
56
56
 
57
57
  class CronManager:
58
58
  _list_default = [
59
- Job('cleanup-task-history',task_cleanup_history, interval=5 * 60),
60
- Job('cleanup-kb-data',kb_cleanup_data_file, interval=180 * 60),
61
- Job('cleanup-chat-attachment',chat_cleanup_attachment, interval=120 * 60),
59
+ Job('cleanup-task-history',task_cleanup_history, interval=4 * 60 * 60),
60
+ Job('cleanup-kb-data',kb_cleanup_data_file, interval=8 * 60 * 60),
61
+ Job('cleanup-chat-attachment',chat_cleanup_attachment, interval=6 * 60 * 60),
62
62
  ]
63
63
  def __get_jobstore_strategy(self) -> JobstoreStrategy:
64
64
  if config.robot_cron_strategy == 'memory':
@@ -1,12 +1,17 @@
1
1
  from langchain_core.documents import Document
2
2
  from langchain_text_splitters import CharacterTextSplitter
3
+ import logging
3
4
 
4
5
  class DocumentChunker:
6
+ _MAX_CHUNK_SIZE = 10_000
5
7
  @staticmethod
6
8
  def chunk(documents: list[Document]) -> list[Document]:
7
- text_splitter = CharacterTextSplitter(chunk_size=10_000, chunk_overlap=500)
9
+ text_splitter = CharacterTextSplitter(chunk_size=DocumentChunker._MAX_CHUNK_SIZE, chunk_overlap=int(DocumentChunker._MAX_CHUNK_SIZE * 0.02))
8
10
  chunked_documents = []
9
11
  for doc in documents:
12
+ if len(doc.page_content) <= DocumentChunker._MAX_CHUNK_SIZE:
13
+ chunked_documents.append(doc)
14
+ continue
10
15
  chunks = text_splitter.split_text(doc.page_content)
11
16
  for chunk in chunks:
12
17
  chunked_documents.append(
@@ -1,7 +1,6 @@
1
1
  import os, logging
2
2
  from ws_bom_robot_app.config import config
3
3
  from datetime import datetime, timedelta
4
- from ws_bom_robot_app.task_manager import task_manager
5
4
 
6
5
  def _cleanup_data_file(folders: list[str], retention: float) -> dict:
7
6
  """
@@ -78,4 +77,5 @@ def task_cleanup_history() -> None:
78
77
  """
79
78
  clean up task queue
80
79
  """
80
+ from ws_bom_robot_app.task_manager import task_manager
81
81
  task_manager.cleanup_task()
@@ -50,8 +50,8 @@ class VectorDBStrategy(ABC):
50
50
  Asynchronously invokes multiple retrievers in parallel, then merges
51
51
  their results while removing duplicates.
52
52
  """
53
+ MAX_TOKENS_PER_BATCH = 300_000 * 0.8
53
54
  def __init__(self):
54
- self.max_tokens_per_batch = 300_000 * 0.8 # conservative limit below 300k openai limit: https://platform.openai.com/docs/api-reference/embeddings/create
55
55
  try:
56
56
  self.encoding = tiktoken.get_encoding("cl100k_base") # text-embedding-3-small, text-embedding-3-large: https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken
57
57
  except Exception:
@@ -78,7 +78,7 @@ class VectorDBStrategy(ABC):
78
78
  for doc in documents:
79
79
  doc_tokens = self._count_tokens(doc.page_content)
80
80
  # check if adding this document exceeds the limit
81
- if current_token_count + doc_tokens > self.max_tokens_per_batch:
81
+ if current_token_count + doc_tokens > VectorDBStrategy.MAX_TOKENS_PER_BATCH:
82
82
  # start new batch if current batch is not empty
83
83
  if current_batch:
84
84
  batches.append(current_batch)
@@ -113,6 +113,38 @@ class Loader():
113
113
 
114
114
  #@timer
115
115
  async def load(self) -> list[Document]:
116
+ #region log
117
+ import warnings
118
+ warnings.filterwarnings("ignore", message=".*pin_memory.*no accelerator is found.*")
119
+ log_msg_to_ignore = [
120
+ "Going to convert document batch...",
121
+ "Initializing pipeline for",
122
+ "Accelerator device:",
123
+ "detected formats:",
124
+ ]
125
+ class MessageFilter(logging.Filter):
126
+ def __init__(self, patterns):
127
+ super().__init__()
128
+ self.log_msg_to_ignore = patterns
129
+
130
+ def filter(self, record):
131
+ for pattern in self.log_msg_to_ignore:
132
+ if pattern in record.getMessage():
133
+ return False
134
+ return True
135
+ message_filter = MessageFilter(log_msg_to_ignore)
136
+ loggers_to_filter = [
137
+ 'docling',
138
+ 'docling.document_converter',
139
+ 'docling.datamodel',
140
+ 'docling.datamodel.document',
141
+ 'docling.utils.accelerator_utils',
142
+ 'unstructured'
143
+ ]
144
+ for logger_name in loggers_to_filter:
145
+ logging.getLogger(logger_name).addFilter(message_filter)
146
+ #endregion log
147
+
116
148
  MAX_RETRIES = 3
117
149
  loaders: MergedDataLoader = MergedDataLoader(self.__directory_loader())
118
150
  try:
@@ -132,5 +164,8 @@ class Loader():
132
164
  finally:
133
165
  del _documents
134
166
  finally:
167
+ # Remove logging filters
168
+ for logger_name in loggers_to_filter:
169
+ logging.getLogger(logger_name).removeFilter(message_filter)
135
170
  del loaders
136
171
  gc.collect()
@@ -6,6 +6,7 @@ from langchain_core.runnables import run_in_executor
6
6
  from docling.document_converter import DocumentConverter, InputFormat, PdfFormatOption, ImageFormatOption
7
7
  from docling.datamodel.pipeline_options import PdfPipelineOptions, TableStructureOptions, TableFormerMode, TesseractCliOcrOptions
8
8
  from langchain_community.document_loaders import UnstructuredFileLoader
9
+ from ws_bom_robot_app.llm.vector_store.db.base import VectorDBStrategy
9
10
 
10
11
  class DoclingLoader(BaseLoader):
11
12
  def __init__(self, file_path: str | list[str], **kwargs: Any) -> None:
@@ -37,28 +38,37 @@ class DoclingLoader(BaseLoader):
37
38
  if doc is done:
38
39
  break
39
40
  yield doc # type: ignore[misc]
41
+ def _fallback_loader(self, source: str, error: Exception = None) -> Iterator[Document]:
42
+ if 'fallback' in self._kwargs:
43
+ if issubclass(self._kwargs['fallback'], (BaseLoader, UnstructuredFileLoader)):
44
+ logging.info(f"Using fallback loader {self._kwargs['fallback']} for {source}")
45
+ try:
46
+ loader: Union[BaseLoader, UnstructuredFileLoader] = self._kwargs['fallback'](
47
+ source,
48
+ **{k: v for k, v in self._kwargs.items() if k != 'fallback'}
49
+ )
50
+ yield from loader.lazy_load()
51
+ except Exception as e:
52
+ logging.warning(f"Failed to load document from {source}: {e} | {traceback.format_exc()}")
53
+ else:
54
+ logging.warning(f"Invalid fallback loader {self._kwargs['fallback']}[{type(self._kwargs['fallback'])}] for {source}")
55
+ else:
56
+ logging.warning(f"Failed to load document from {source}: {error}")
40
57
  def lazy_load(self) -> Iterator[Document]:
41
58
  for source in self._file_paths:
42
59
  try:
43
- _result = self._converter.convert(
44
- os.path.abspath(source),
45
- raises_on_error=True)
46
- doc = _result.document
47
- text = doc.export_to_markdown(image_placeholder="")
48
- yield Document(page_content=text, metadata={"source": source})
49
- except Exception as e:
50
- if 'fallback' in self._kwargs:
51
- if issubclass(self._kwargs['fallback'], (BaseLoader, UnstructuredFileLoader)):
52
- logging.info(f"Using fallback loader {self._kwargs['fallback']} for {source}")
53
- try:
54
- loader: Union[BaseLoader, UnstructuredFileLoader] = self._kwargs['fallback'](
55
- source,
56
- **{k: v for k, v in self._kwargs.items() if k != 'fallback'}
57
- )
58
- yield from loader.lazy_load()
59
- except Exception as e:
60
- logging.warning(f"Failed to load document from {source}: {e} | {traceback.format_exc()}")
61
- else:
62
- logging.warning(f"Invalid fallback loader {self._kwargs['fallback']}[{type(self._kwargs['fallback'])}] for {source}")
60
+ #manage only small file with header, preventing header stripping and improper chunking
61
+ if (source.endswith('.csv') or source.endswith('.xlsx')) \
62
+ and 'fallback' in self._kwargs \
63
+ and os.path.getsize(source) > (VectorDBStrategy.MAX_TOKENS_PER_BATCH // 4): #rough token estimate
64
+ yield from self._fallback_loader(source)
63
65
  else:
64
- logging.warning(f"Failed to load document from {source}: {e} | {traceback.format_exc()}")
66
+ _result = self._converter.convert(
67
+ os.path.abspath(source),
68
+ raises_on_error=True)
69
+ doc = _result.document
70
+ text = doc.export_to_markdown(image_placeholder="")
71
+ yield Document(page_content=text, metadata={"source": source})
72
+ except Exception as e:
73
+ yield from self._fallback_loader(source,e)
74
+
@@ -0,0 +1,156 @@
1
+ import datetime
2
+ from fastapi import FastAPI
3
+ from ws_bom_robot_app.util import is_app_subprocess
4
+ _uptime = datetime.datetime.now()
5
+ app = FastAPI(redoc_url=None,docs_url=None,openapi_url=None)
6
+
7
+ if not is_app_subprocess():
8
+ import platform
9
+ from fastapi.responses import FileResponse
10
+ import os, sys
11
+ from fastapi import Depends
12
+ from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
13
+ from fastapi.openapi.utils import get_openapi
14
+ from ws_bom_robot_app.auth import authenticate
15
+ from ws_bom_robot_app.config import config
16
+ from ws_bom_robot_app.util import _log
17
+ from ws_bom_robot_app.llm.api import router as llm
18
+ from ws_bom_robot_app.task_manager import router as task
19
+ from ws_bom_robot_app.cron_manager import (
20
+ router as cron,
21
+ cron_manager)
22
+ cron_manager.start()
23
+ app.include_router(llm,dependencies=[Depends(authenticate)])
24
+ app.include_router(task,dependencies=[Depends(authenticate)])
25
+ app.include_router(cron,dependencies=[Depends(authenticate)])
26
+
27
+ @app.get("/")
28
+ async def root():
29
+ return health()
30
+ @app.get("/favicon.ico")
31
+ async def favicon():
32
+ return FileResponse("./favicon.ico")
33
+
34
+ @app.get("/docs", include_in_schema=False)
35
+ async def get_swagger_documentation(authenticate: bool = Depends(authenticate)):
36
+ return get_swagger_ui_html(openapi_url="/openapi.json", title="docs")
37
+ @app.get("/redoc", include_in_schema=False)
38
+ async def get_redoc_documentation(authenticate: bool = Depends(authenticate)):
39
+ return get_redoc_html(openapi_url="/openapi.json", title="docs")
40
+ @app.get("/openapi.json", include_in_schema=False)
41
+ async def openapi(authenticate: bool = Depends(authenticate)):
42
+ return get_openapi(title=app.title, version=app.version, routes=app.routes)
43
+
44
+ @app.get("/api/health",tags=["diag"])
45
+ def health():
46
+ return {"status": "ok"}
47
+ def __get_size(bytes, suffix="B"):
48
+ """
49
+ Scale bytes to its proper format
50
+ e.g:
51
+ 1253656 => '1.20MB'
52
+ 1253656678 => '1.17GB'
53
+ """
54
+ factor = 1024
55
+ for unit in ["", "K", "M", "G", "T", "P"]:
56
+ if bytes < factor:
57
+ return f"{bytes:.2f}{unit}{suffix}"
58
+ bytes /= factor
59
+ def __get_disk_info():
60
+ import psutil
61
+ partitions = psutil.disk_partitions()
62
+ _disks:list = []
63
+ for partition in partitions:
64
+ device = partition.device
65
+ mountpoint = partition.mountpoint
66
+ fstype = partition.fstype
67
+ try:
68
+ usage = psutil.disk_usage(mountpoint)
69
+ except PermissionError:
70
+ continue
71
+ total = __get_size(usage.total)
72
+ used = __get_size(usage.used)
73
+ free = __get_size(usage.free)
74
+ percent = f"{usage.percent}%"
75
+ _disks.append({"device": device, "mountpoint": mountpoint, "fstype": fstype, "total": total, "used": used, "free": free, "percent": percent})
76
+ return _disks
77
+ @app.get("/api/diag",tags=["diag"])
78
+ def diag(authenticate: bool = Depends(authenticate)):
79
+ import importlib,psutil
80
+ from ws_bom_robot_app.llm.providers.llm_manager import LlmManager as wsllm
81
+ from ws_bom_robot_app.llm.vector_store.db.manager import VectorDbManager as wsdb
82
+ from ws_bom_robot_app.llm.vector_store.loader.base import Loader as wsldr
83
+ from ws_bom_robot_app.llm.vector_store.integration.manager import IntegrationManager as wsim
84
+ from ws_bom_robot_app.llm.tools.tool_manager import ToolManager as wstm
85
+ from ws_bom_robot_app.llm.agent_description import AgentDescriptor as wsad
86
+
87
+ svmem = psutil.virtual_memory()
88
+ swap = psutil.swap_memory()
89
+ try:
90
+ ws_bom_robot_app_version = importlib.metadata.version("ws_bom_robot_app")
91
+ except:
92
+ ws_bom_robot_app_version = "unknown"
93
+ peer_process_ids = [c.pid for c in psutil.Process(os.getppid()).children()] if config.runtime_options().is_multi_process else None
94
+ return {
95
+ "status":"ok",
96
+ "uptime": {'from':_uptime,'elapsed':str(datetime.datetime.now()-_uptime)},
97
+ "system": {
98
+ "platform": {
99
+ "node": platform.node(),
100
+ "system": platform.system(),
101
+ "platform": platform.platform(),
102
+ "version": platform.version(),
103
+ "type": platform.machine(),
104
+ "processor": platform.processor(),
105
+ "architecture": platform.architecture()
106
+ },
107
+ "cpu": {
108
+ "physical_core": psutil.cpu_count(logical=False),
109
+ "total_core": psutil.cpu_count(logical=True),
110
+ "load": f"{psutil.cpu_percent(interval=1)}%"
111
+ },
112
+ "memory": {
113
+ "total": f"{__get_size(svmem.total)}",
114
+ "available": f"{__get_size(svmem.available)}",
115
+ "used": f"{__get_size(svmem.used)}",
116
+ "free": f"{__get_size(svmem.free)}",
117
+ "percent": f"{svmem.percent}%"
118
+ },
119
+ "swap": {
120
+ "total": f"{__get_size(swap.total)}",
121
+ "used": f"{__get_size(swap.used)}",
122
+ "free": f"{__get_size(swap.free)}",
123
+ "percent": f"{swap.percent}%"
124
+ },
125
+ "disk": __get_disk_info(),
126
+ "sys": {
127
+ "version": sys.version,
128
+ "platform": sys.platform,
129
+ "executable": sys.executable,
130
+ "args": {k: arg for k, arg in enumerate(sys.argv)}
131
+ },
132
+ "os": {
133
+ "ppid": os.getppid(),
134
+ "pid": os.getpid(),
135
+ "pids": peer_process_ids,
136
+ "cwd": os.getcwd(),
137
+ "ws_bom_robot_app": ws_bom_robot_app_version,
138
+ "env": os.environ,
139
+ },
140
+ },
141
+ "config":config,
142
+ "runtime":config.runtime_options(),
143
+ "extension": {
144
+ "provider":({item[0]: type(item[1]).__name__} for item in wsllm._list.items()),
145
+ "db":({item[0]: type(item[1]).__name__} for item in wsdb._list.items()),
146
+ "loader": ({item[0]: item[1].loader.__name__ if item[1] else None} for item in sorted(wsldr._list.items(), key=lambda x: x[0]) if item[1]),
147
+ "integration":({item[0]: type(item[1]).__name__} for item in wsim._list.items()),
148
+ "tool": ({item[0]: item[1].function.__name__} for item in wstm._list.items()),
149
+ "agent":({item[0]: type(item[1]).__name__} for item in wsad._list.items())
150
+ }
151
+ }
152
+ @app.post("/diag/reload",tags=["diag"])
153
+ def reset(authenticate: bool = Depends(authenticate)):
154
+ _log.info("restart server")
155
+ with open(".reloadfile","w") as f:
156
+ f.write("")
@@ -12,6 +12,9 @@ def _worker_run_pickled(serialized_task: bytes, conn: Connection):
12
12
  capture return value or exception and send back via conn.send((ok_flag, payload_serialized)).
13
13
  This runs in a separate process and must be top-level for multiprocessing.
14
14
  """
15
+ import os
16
+ # mark as a subprocess
17
+ os.environ['IS_ROBOT_APP_SUBPROCESS'] = 'true'
15
18
  try:
16
19
  if _pickler is None:
17
20
  raise RuntimeError("No pickler available in worker process.")
@@ -69,6 +69,7 @@ class TaskStatus(IdentifiableEntity):
69
69
  result: Optional[T] = None
70
70
  metadata: TaskMetaData = None
71
71
  error: Optional[str] = None
72
+ retry: int = 0
72
73
  model_config = ConfigDict(
73
74
  arbitrary_types_allowed=True
74
75
  )
@@ -118,7 +119,8 @@ class TaskEntry(IdentifiableEntity):
118
119
  class TaskStatistics(BaseModel):
119
120
  class TaskStatisticExecutionInfo(BaseModel):
120
121
  retention_days: float = config.robot_task_retention_days
121
- max_concurrent: int
122
+ max_parallelism: int
123
+ slot_available: int
122
124
  pid: int = os.getpid()
123
125
  running: list[TaskStatus]
124
126
  slowest: list
@@ -137,9 +139,12 @@ class TaskStatistics(BaseModel):
137
139
 
138
140
  #region interface
139
141
  class TaskManagerStrategy(ABC):
140
- def __init__(self, max_concurrent_tasks: int = max(1,floor(config.robot_task_max_total_parallelism / config.runtime_options().number_of_workers))):
141
- self.max_concurrent_tasks = max_concurrent_tasks
142
- self.semaphore = asyncio.Semaphore(self.max_concurrent_tasks)
142
+ def __init__(self, max_concurrent_tasks: Optional[int] = None):
143
+ if max_concurrent_tasks is None:
144
+ workers = config.runtime_options().number_of_workers
145
+ max_concurrent_tasks = max(1, floor(config.robot_task_max_total_parallelism / max(1, workers)))
146
+ self.max_parallelism = max_concurrent_tasks
147
+ self.semaphore = asyncio.Semaphore(max_concurrent_tasks)
143
148
  self.running_tasks = dict[str, TaskEntry]()
144
149
  self.loop = asyncio.get_event_loop()
145
150
 
@@ -201,14 +206,15 @@ class TaskManagerStrategy(ABC):
201
206
  #remove from running tasks
202
207
  if task_entry.id in self.running_tasks:
203
208
  del self.running_tasks[task_entry.id]
204
- #notify webhooks
205
- if task_entry.headers and task_entry.headers.x_ws_bom_webhooks:
206
- try:
207
- asyncio.create_task(
208
- WebhookNotifier().notify_webhook(task_entry.status, task_entry.headers.x_ws_bom_webhooks)
209
- )
210
- except Exception as e:
211
- _log.error(f"Failed to schedule webhook notification for task {task_entry.id}: {e}")
209
+ #notify webhooks: a task has completed or failed, if failed with retry policy the task remains in pending state, and will not be notified until complete/failure
210
+ if task_entry.status.status in ["completed","failure"]:
211
+ if task_entry.headers and task_entry.headers.x_ws_bom_webhooks:
212
+ try:
213
+ asyncio.create_task(
214
+ WebhookNotifier().notify_webhook(task_entry.status, task_entry.headers.x_ws_bom_webhooks)
215
+ )
216
+ except Exception as e:
217
+ _log.error(f"Failed to schedule webhook notification for task {task_entry.id}: {e}")
212
218
 
213
219
  def task_done_callback(self, task_entry: TaskEntry) -> Callable:
214
220
  def callback(task: asyncio.Task, context: Any | None = None):
@@ -260,6 +266,55 @@ class TaskManagerStrategy(ABC):
260
266
  async with self.semaphore:
261
267
  await self._execute_task(task_entry)
262
268
 
269
+ async def _monitor_subprocess(self, task_entry: TaskEntry, proc, conn):
270
+ try:
271
+ # Wait for the worker to send bytes (this blocks, so run via executor wrapper)
272
+ data_bytes = await _recv_from_connection_async(conn)
273
+ # unpickle bytes to get payload
274
+ try:
275
+ payload = _pickler.loads(data_bytes)
276
+ except Exception:
277
+ # fallback if pickler fails
278
+ payload = ("err", {"error": "Failed to unpickle subprocess result"})
279
+ if isinstance(payload, tuple) and payload[0] == "ok":
280
+ result = payload[1]
281
+ # write results into task_entry
282
+ self._update_task_by_event(task_entry, "completed", result)
283
+ else:
284
+ # error
285
+ err_info = payload[1]["error"] if isinstance(payload, tuple) else str(payload)
286
+ self._update_task_by_event(task_entry, "failure", err_info) # give up, no retry
287
+ except Exception:
288
+ # maybe subprocess is no more alive / killed due to memory pressure
289
+ if task_entry.status.retry < config.robot_task_mp_max_retries:
290
+ task_entry.status.retry += 1
291
+ _log.warning(f"Task {task_entry.id} failure, retrying {task_entry.status.retry}...")
292
+ async def delayed_retry():
293
+ _delay = config.robot_task_mp_retry_delay # help to backpressure when overloaded
294
+ if self.semaphore._value > 0: # free semaphore slots available
295
+ _delay = 5 # small/no delay if retry can run immediately
296
+ await asyncio.sleep(_delay) # delay in seconds
297
+ await self._run_task_with_semaphore(task_entry)
298
+ asyncio.create_task(delayed_retry())
299
+ # semaphore is released, so new task can be executed
300
+ return
301
+ else:
302
+ self._update_task_by_event(task_entry, "failure", "subprocess monitor error: failed to receive data from connection")
303
+ finally:
304
+ # ensure process termination / cleanup
305
+ try:
306
+ conn.close()
307
+ except Exception:
308
+ pass
309
+ try:
310
+ if proc.is_alive():
311
+ proc.terminate()
312
+ proc.join(timeout=1)
313
+ except Exception:
314
+ pass
315
+ # callback
316
+ self._update_task_by_event(task_entry, "callback", None)
317
+
263
318
  async def _execute_task(self, task_entry: TaskEntry):
264
319
  """
265
320
  Execute the task. Try to run it inside a subprocess (if serializable).
@@ -269,56 +324,19 @@ class TaskManagerStrategy(ABC):
269
324
  """
270
325
  self.running_tasks[task_entry.id]=task_entry
271
326
  task_entry.status.metadata.start_at = str(datetime.now().isoformat())
272
- # Try to spawn subprocess (non-blocking)
327
+ # try to spawn subprocess (non-blocking)
273
328
  can_use_subprocess = task_entry.status.metadata.extra.get("can_use_subprocess", False)
274
329
  if config.robot_task_mp_enable and can_use_subprocess:
275
330
  proc, conn, used_subprocess = _start_subprocess_for_coroutine(task_entry.coroutine)
276
331
  if used_subprocess and proc is not None and conn is not None:
277
- # We will monitor the subprocess asynchronously
332
+ # monitor subprocess asynchronously
278
333
  task_entry.status.status = "pending"
279
334
  task_entry.status.metadata.pid_child = proc.pid
280
335
  _log.info(f"Task {task_entry.id} started in subprocess (pid={proc.pid})")
281
-
282
- async def _monitor_subprocess():
283
- try:
284
- # Wait for the worker to send bytes (this blocks, so run via executor wrapper)
285
- data_bytes = await _recv_from_connection_async(conn)
286
- # unpickle bytes to get payload
287
- try:
288
- payload = _pickler.loads(data_bytes)
289
- except Exception:
290
- # fallback if pickler fails
291
- payload = ("err", {"error": "Failed to unpickle subprocess result"})
292
- if isinstance(payload, tuple) and payload[0] == "ok":
293
- result = payload[1]
294
- # write results into task_entry
295
- self._update_task_by_event(task_entry, "completed", result)
296
- else:
297
- # error
298
- err_info = payload[1]["error"] if isinstance(payload, tuple) else str(payload)
299
- self._update_task_by_event(task_entry, "failure", err_info)
300
- except Exception:
301
- # maybe subprocess is no more alive / killed
302
- self._update_task_by_event(task_entry, "failure", "subprocess monitor error: failed to receive data from connection")
303
- finally:
304
- # Ensure process termination / cleanup
305
- try:
306
- conn.close()
307
- except Exception:
308
- pass
309
- try:
310
- if proc.is_alive():
311
- proc.terminate()
312
- proc.join(timeout=1)
313
- except Exception:
314
- pass
315
- # callback
316
- self._update_task_by_event(task_entry, "callback", None)
317
-
318
- # schedule monitor task and return
319
- asyncio.create_task(_monitor_subprocess())
320
- return
321
- # fallback
336
+ # await monitor process, then return: important to acquire semaphore
337
+ await self._monitor_subprocess(task_entry, proc, conn)
338
+ return
339
+ # default fallback (in-process)
322
340
  try:
323
341
  async def _callable_to_coroutine(func: Any) -> Any:
324
342
  if callable(func) and not inspect.iscoroutine(func):
@@ -373,7 +391,8 @@ class TaskManagerStrategy(ABC):
373
391
  ),
374
392
  exec_info=TaskStatistics.TaskStatisticExecutionInfo(
375
393
  retention_days=config.robot_task_retention_days,
376
- max_concurrent=self.max_concurrent_tasks,
394
+ max_parallelism=self.max_parallelism,
395
+ slot_available=self.semaphore._value,
377
396
  running=[task.status for task in self.running_task()],
378
397
  slowest=_slowest
379
398
  )
@@ -383,8 +402,8 @@ class TaskManagerStrategy(ABC):
383
402
 
384
403
  #region memory implementation
385
404
  class MemoryTaskManagerStrategy(TaskManagerStrategy):
386
- def __init__(self):
387
- super().__init__()
405
+ def __init__(self, max_concurrent_tasks: Optional[int] = None):
406
+ super().__init__(max_concurrent_tasks)
388
407
  self.tasks: Dict[str, TaskEntry] = {}
389
408
 
390
409
  def create_task(self, coroutine: Any, headers: TaskHeader | None = None) -> IdentifiableEntity:
@@ -425,8 +444,8 @@ class TaskEntryModel(Base):
425
444
  arbitrary_types_allowed=True
426
445
  )
427
446
  class DatabaseTaskManagerStrategy(TaskManagerStrategy):
428
- def __init__(self, db_url: str = f"sqlite:///{config.robot_data_folder}/db/tasks.sqlite"):
429
- super().__init__()
447
+ def __init__(self, db_url: str = f"sqlite:///{config.robot_data_folder}/db/tasks.sqlite", max_concurrent_tasks: Optional[int] = None):
448
+ super().__init__(max_concurrent_tasks)
430
449
  self.engine = create_engine(db_url)
431
450
  self.Session = sessionmaker(bind=self.engine)
432
451
  Base.metadata.create_all(self.engine)
@@ -22,6 +22,12 @@ def logger_instance(name: str) -> logging.Logger:
22
22
  _log: logging.Logger = locals().get("_loc", logger_instance(__name__))
23
23
  #endregion
24
24
 
25
+ #region task
26
+ def is_app_subprocess():
27
+ """Check if we're running a task in a subprocess."""
28
+ return os.environ.get('IS_ROBOT_APP_SUBPROCESS', '').lower() == 'true'
29
+ #endregion
30
+
25
31
  #region cache
26
32
  _cache = {}
27
33
  _cache_timestamps = {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ws_bom_robot_app
3
- Version: 0.0.83
3
+ Version: 0.0.85
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
@@ -1,157 +0,0 @@
1
- import datetime
2
- import platform
3
- from fastapi.responses import FileResponse
4
- import uvicorn, os, sys
5
- from fastapi import FastAPI, Depends
6
- from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
7
- from fastapi.openapi.utils import get_openapi
8
- from ws_bom_robot_app.auth import authenticate
9
- from ws_bom_robot_app.config import config
10
- from ws_bom_robot_app.llm.api import router as llm
11
- from ws_bom_robot_app.task_manager import router as task
12
- from ws_bom_robot_app.cron_manager import (
13
- router as cron,
14
- cron_manager)
15
- from ws_bom_robot_app.util import _log
16
-
17
- _uptime = datetime.datetime.now()
18
- cron_manager.start()
19
- app = FastAPI(redoc_url=None,docs_url=None,openapi_url=None)
20
- app.include_router(llm,dependencies=[Depends(authenticate)])
21
- app.include_router(task,dependencies=[Depends(authenticate)])
22
- app.include_router(cron,dependencies=[Depends(authenticate)])
23
-
24
- @app.get("/")
25
- async def root():
26
- return health()
27
- @app.get("/favicon.ico")
28
- async def favicon():
29
- return FileResponse("./favicon.ico")
30
-
31
- @app.get("/docs", include_in_schema=False)
32
- async def get_swagger_documentation(authenticate: bool = Depends(authenticate)):
33
- return get_swagger_ui_html(openapi_url="/openapi.json", title="docs")
34
- @app.get("/redoc", include_in_schema=False)
35
- async def get_redoc_documentation(authenticate: bool = Depends(authenticate)):
36
- return get_redoc_html(openapi_url="/openapi.json", title="docs")
37
- @app.get("/openapi.json", include_in_schema=False)
38
- async def openapi(authenticate: bool = Depends(authenticate)):
39
- return get_openapi(title=app.title, version=app.version, routes=app.routes)
40
-
41
- @app.get("/api/health",tags=["diag"])
42
- def health():
43
- return {"status": "ok"}
44
- def __get_size(bytes, suffix="B"):
45
- """
46
- Scale bytes to its proper format
47
- e.g:
48
- 1253656 => '1.20MB'
49
- 1253656678 => '1.17GB'
50
- """
51
- factor = 1024
52
- for unit in ["", "K", "M", "G", "T", "P"]:
53
- if bytes < factor:
54
- return f"{bytes:.2f}{unit}{suffix}"
55
- bytes /= factor
56
- def __get_disk_info():
57
- import psutil
58
- partitions = psutil.disk_partitions()
59
- _disks:list = []
60
- for partition in partitions:
61
- device = partition.device
62
- mountpoint = partition.mountpoint
63
- fstype = partition.fstype
64
- try:
65
- usage = psutil.disk_usage(mountpoint)
66
- except PermissionError:
67
- continue
68
- total = __get_size(usage.total)
69
- used = __get_size(usage.used)
70
- free = __get_size(usage.free)
71
- percent = f"{usage.percent}%"
72
- _disks.append({"device": device, "mountpoint": mountpoint, "fstype": fstype, "total": total, "used": used, "free": free, "percent": percent})
73
- return _disks
74
- @app.get("/api/diag",tags=["diag"])
75
- def diag(authenticate: bool = Depends(authenticate)):
76
- import importlib,psutil
77
- from ws_bom_robot_app.llm.providers.llm_manager import LlmManager as wsllm
78
- from ws_bom_robot_app.llm.vector_store.db.manager import VectorDbManager as wsdb
79
- from ws_bom_robot_app.llm.vector_store.loader.base import Loader as wsldr
80
- from ws_bom_robot_app.llm.vector_store.integration.manager import IntegrationManager as wsim
81
- from ws_bom_robot_app.llm.tools.tool_manager import ToolManager as wstm
82
- from ws_bom_robot_app.llm.agent_description import AgentDescriptor as wsad
83
-
84
- svmem = psutil.virtual_memory()
85
- swap = psutil.swap_memory()
86
- try:
87
- ws_bom_robot_app_version = importlib.metadata.version("ws_bom_robot_app")
88
- except:
89
- ws_bom_robot_app_version = "unknown"
90
- peer_process_ids = [c.pid for c in psutil.Process(os.getppid()).children()] if config.runtime_options().is_multi_process else None
91
- return {
92
- "status":"ok",
93
- "uptime": {'from':_uptime,'elapsed':str(datetime.datetime.now()-_uptime)},
94
- "system": {
95
- "platform": {
96
- "node": platform.node(),
97
- "system": platform.system(),
98
- "platform": platform.platform(),
99
- "version": platform.version(),
100
- "type": platform.machine(),
101
- "processor": platform.processor(),
102
- "architecture": platform.architecture()
103
- },
104
- "cpu": {
105
- "physical_core": psutil.cpu_count(logical=False),
106
- "total_core": psutil.cpu_count(logical=True),
107
- "load": f"{psutil.cpu_percent(interval=1)}%"
108
- },
109
- "memory": {
110
- "total": f"{__get_size(svmem.total)}",
111
- "available": f"{__get_size(svmem.available)}",
112
- "used": f"{__get_size(svmem.used)}",
113
- "free": f"{__get_size(svmem.free)}",
114
- "percent": f"{svmem.percent}%"
115
- },
116
- "swap": {
117
- "total": f"{__get_size(swap.total)}",
118
- "used": f"{__get_size(swap.used)}",
119
- "free": f"{__get_size(swap.free)}",
120
- "percent": f"{swap.percent}%"
121
- },
122
- "disk": __get_disk_info(),
123
- "sys": {
124
- "version": sys.version,
125
- "platform": sys.platform,
126
- "executable": sys.executable,
127
- "args": {k: arg for k, arg in enumerate(sys.argv)}
128
- },
129
- "os": {
130
- "ppid": os.getppid(),
131
- "pid": os.getpid(),
132
- "pids": peer_process_ids,
133
- "cwd": os.getcwd(),
134
- "ws_bom_robot_app": ws_bom_robot_app_version,
135
- "env": os.environ,
136
- },
137
- },
138
- "config":config,
139
- "runtime":config.runtime_options(),
140
- "extension": {
141
- "provider":({item[0]: type(item[1]).__name__} for item in wsllm._list.items()),
142
- "db":({item[0]: type(item[1]).__name__} for item in wsdb._list.items()),
143
- "loader": ({item[0]: item[1].loader.__name__ if item[1] else None} for item in sorted(wsldr._list.items(), key=lambda x: x[0]) if item[1]),
144
- "integration":({item[0]: type(item[1]).__name__} for item in wsim._list.items()),
145
- "tool": ({item[0]: item[1].function.__name__} for item in wstm._list.items()),
146
- "agent":({item[0]: type(item[1]).__name__} for item in wsad._list.items())
147
- }
148
- }
149
- @app.post("/diag/reload",tags=["diag"])
150
- def reset(authenticate: bool = Depends(authenticate)):
151
- _log.info("restart server")
152
- with open(".reloadfile","w") as f:
153
- f.write("")
154
-
155
- # Start the FastAPI server
156
- if __name__ == "__main__":
157
- uvicorn.run(app, host="0.0.0.0", port=6001, env_file="../.env", reload=True, log_level="debug")