ws-bom-robot-app 0.0.81__py3-none-any.whl → 0.0.83__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. ws_bom_robot_app/config.py +10 -0
  2. ws_bom_robot_app/cron_manager.py +6 -6
  3. ws_bom_robot_app/llm/agent_description.py +123 -123
  4. ws_bom_robot_app/llm/agent_handler.py +166 -166
  5. ws_bom_robot_app/llm/agent_lcel.py +50 -50
  6. ws_bom_robot_app/llm/api.py +2 -2
  7. ws_bom_robot_app/llm/defaut_prompt.py +15 -15
  8. ws_bom_robot_app/llm/feedbacks/feedback_manager.py +66 -66
  9. ws_bom_robot_app/llm/main.py +158 -158
  10. ws_bom_robot_app/llm/models/feedback.py +30 -30
  11. ws_bom_robot_app/llm/nebuly_handler.py +185 -185
  12. ws_bom_robot_app/llm/providers/llm_manager.py +5 -6
  13. ws_bom_robot_app/llm/tools/tool_builder.py +65 -65
  14. ws_bom_robot_app/llm/tools/tool_manager.py +330 -330
  15. ws_bom_robot_app/llm/tools/utils.py +41 -41
  16. ws_bom_robot_app/llm/utils/agent.py +34 -34
  17. ws_bom_robot_app/llm/utils/cleanup.py +7 -0
  18. ws_bom_robot_app/llm/utils/cms.py +114 -114
  19. ws_bom_robot_app/llm/utils/download.py +183 -185
  20. ws_bom_robot_app/llm/utils/print.py +29 -29
  21. ws_bom_robot_app/llm/vector_store/generator.py +137 -137
  22. ws_bom_robot_app/llm/vector_store/integration/azure.py +1 -1
  23. ws_bom_robot_app/llm/vector_store/integration/base.py +57 -15
  24. ws_bom_robot_app/llm/vector_store/integration/confluence.py +1 -1
  25. ws_bom_robot_app/llm/vector_store/integration/dropbox.py +1 -1
  26. ws_bom_robot_app/llm/vector_store/integration/gcs.py +1 -1
  27. ws_bom_robot_app/llm/vector_store/integration/github.py +22 -22
  28. ws_bom_robot_app/llm/vector_store/integration/googledrive.py +1 -1
  29. ws_bom_robot_app/llm/vector_store/integration/jira.py +93 -60
  30. ws_bom_robot_app/llm/vector_store/integration/s3.py +1 -1
  31. ws_bom_robot_app/llm/vector_store/integration/sftp.py +1 -1
  32. ws_bom_robot_app/llm/vector_store/integration/sharepoint.py +7 -14
  33. ws_bom_robot_app/llm/vector_store/integration/shopify.py +143 -144
  34. ws_bom_robot_app/llm/vector_store/integration/sitemap.py +3 -0
  35. ws_bom_robot_app/llm/vector_store/integration/slack.py +3 -2
  36. ws_bom_robot_app/llm/vector_store/integration/thron.py +102 -103
  37. ws_bom_robot_app/llm/vector_store/loader/base.py +8 -6
  38. ws_bom_robot_app/llm/vector_store/loader/docling.py +1 -1
  39. ws_bom_robot_app/llm/vector_store/loader/json_loader.py +25 -25
  40. ws_bom_robot_app/subprocess_runner.py +103 -0
  41. ws_bom_robot_app/task_manager.py +169 -41
  42. {ws_bom_robot_app-0.0.81.dist-info → ws_bom_robot_app-0.0.83.dist-info}/METADATA +19 -9
  43. ws_bom_robot_app-0.0.83.dist-info/RECORD +74 -0
  44. ws_bom_robot_app-0.0.81.dist-info/RECORD +0 -73
  45. {ws_bom_robot_app-0.0.81.dist-info → ws_bom_robot_app-0.0.83.dist-info}/WHEEL +0 -0
  46. {ws_bom_robot_app-0.0.81.dist-info → ws_bom_robot_app-0.0.83.dist-info}/top_level.txt +0 -0
@@ -1,185 +1,183 @@
1
- from typing import List,Optional
2
- import os, logging, aiohttp, asyncio, hashlib, json
3
- import uuid
4
- from pydantic import BaseModel
5
- import base64, requests, mimetypes
6
- from urllib.parse import urlparse
7
- from tqdm.asyncio import tqdm
8
- from ws_bom_robot_app.config import config
9
- import aiofiles
10
-
11
- async def download_files(urls: List[str], destination_folder: str, authorization: str = None):
12
- tasks = [download_file(file, os.path.join(destination_folder, os.path.basename(file)), authorization=authorization) for file in urls]
13
- results = await asyncio.gather(*tasks, return_exceptions=False)
14
- for i, result in enumerate(results):
15
- if not result:
16
- raise Exception(f"Download failed for file: {urls[i]}")
17
-
18
- async def download_file(url: str, destination: str, chunk_size: int = 8192, authorization: str = None) -> Optional[str]:
19
- """
20
- Downloads a file from a given URL to a destination path asynchronously.
21
-
22
- Args:
23
- url: The URL of the file to download
24
- destination: The local path where the file should be saved
25
- chunk_size: Size of chunks to download (default: 8192 bytes)
26
-
27
- Returns:
28
- str: Path to the downloaded file if successful, None otherwise
29
-
30
- Raises:
31
- Various exceptions are caught and logged
32
- """
33
- try:
34
- # Ensure the destination directory exists
35
- os.makedirs(os.path.dirname(os.path.abspath(destination)), exist_ok=True)
36
-
37
- async with aiohttp.ClientSession() as session:
38
- if authorization:
39
- headers = {'Authorization': authorization}
40
- session.headers.update(headers)
41
- async with session.get(url) as response:
42
- # Check if the request was successful
43
- if response.status != 200:
44
- logging.error(f"Failed to download file. Status code: {response.status}")
45
- return None
46
-
47
- # Get the total file size if available
48
- total_size = int(response.headers.get('content-length', 0))
49
- # Open the destination file and write chunks
50
- with open(destination, 'wb') as f:
51
- with tqdm(
52
- total=total_size,
53
- desc="Downloading",
54
- unit='B',
55
- unit_scale=True,
56
- unit_divisor=1024
57
- ) as pbar:
58
- async for chunk in response.content.iter_chunked(chunk_size):
59
- if chunk:
60
- f.write(chunk)
61
- pbar.update(len(chunk))
62
-
63
- logging.info(f"File downloaded successfully to {destination}")
64
- return destination
65
-
66
- except aiohttp.ClientError as e:
67
- logging.error(f"Network error occurred: {str(e)}")
68
- return None
69
- except asyncio.TimeoutError:
70
- logging.error("Download timed out")
71
- return None
72
- except IOError as e:
73
- logging.error(f"IO error occurred: {str(e)}")
74
- return None
75
- except Exception as e:
76
- logging.error(f"Unexpected error occurred: {str(e)}")
77
- return None
78
- finally:
79
- # If download failed and file was partially created, clean it up
80
- if os.path.exists(destination) and os.path.getsize(destination) == 0:
81
- try:
82
- os.remove(destination)
83
- logging.info(f"Cleaned up incomplete download: {destination}")
84
- except OSError:
85
- pass
86
-
87
- # ensuse attachment folder exists
88
- os.makedirs(os.path.join(config.robot_data_folder, config.robot_data_attachment_folder), exist_ok=True)
89
- class Base64File(BaseModel):
90
- """Base64 encoded file representation"""
91
- url: str
92
- base64_url: str
93
- base64_content: str
94
- name: str
95
- extension: str
96
- mime_type: str
97
-
98
- @staticmethod
99
- def _is_base64_data_uri(url: str) -> bool:
100
- """Check if URL is already a base64 data URI"""
101
- return (isinstance(url, str) and
102
- url.startswith('data:') and
103
- ';base64,' in url and
104
- len(url.split(',')) == 2)
105
-
106
- async def from_url(url: str) -> "Base64File":
107
- """Download file and return as base64 data URI"""
108
- def _cache_file(url: str) -> str:
109
- _hash = hashlib.md5(url.encode()).hexdigest()
110
- return os.path.join(config.robot_data_folder, config.robot_data_attachment_folder, f"{_hash}.json")
111
- async def from_cache(url: str) -> "Base64File":
112
- """Check if file is already downloaded and return data"""
113
- _file = _cache_file(url)
114
- if os.path.exists(_file):
115
- try:
116
- async with aiofiles.open(_file, 'rb') as f:
117
- content = await f.read()
118
- return Base64File(**json.loads(content))
119
- except Exception as e:
120
- logging.error(f"Error reading cache file {_file}: {e}")
121
- return None
122
- return None
123
- async def to_cache(file: "Base64File", url: str) -> None:
124
- """Save file to cache"""
125
- _file = _cache_file(url)
126
- try:
127
- async with aiofiles.open(_file, 'wb') as f:
128
- await f.write(file.model_dump_json().encode('utf-8'))
129
- except Exception as e:
130
- logging.error(f"Error writing cache file {_file}: {e}")
131
-
132
- # special case: base64 data URI
133
- if Base64File._is_base64_data_uri(url):
134
- mime_type = url.split(';')[0].replace('data:', '')
135
- base64_content = url.split(',')[1]
136
- extension=mime_type.split('/')[-1]
137
- name = f"file-{uuid.uuid4()}.{extension}"
138
- return Base64File(
139
- url=url,
140
- base64_url=url,
141
- base64_content=base64_content,
142
- name=name,
143
- extension=extension,
144
- mime_type=mime_type
145
- )
146
-
147
- # default download
148
- _error = None
149
- try:
150
- if _content := await from_cache(url):
151
- return _content
152
- async with aiohttp.ClientSession() as session:
153
- async with session.get(url, timeout=aiohttp.ClientTimeout(total=30), headers={"User-Agent": "Mozilla/5.0"}) as response:
154
- print(f"Downloading {url} - Status: {response.status}")
155
- response.raise_for_status()
156
- content = await response.read()
157
- # mime type detection
158
- mime_type = response.headers.get('content-type', '').split(';')[0]
159
- if not mime_type:
160
- mime_type, _ = mimetypes.guess_type(urlparse(url).path)
161
- if not mime_type:
162
- mime_type = 'application/octet-stream'
163
- # to base64
164
- base64_content = base64.b64encode(content).decode('utf-8')
165
- name = url.split('/')[-1]
166
- extension = name.split('.')[-1]
167
- except Exception as e:
168
- _error = f"Failed to download file from {url}: {e}"
169
- logging.error(_error)
170
- base64_content = base64.b64encode(_error.encode('utf-8')).decode('utf-8')
171
- name = "download_error.txt"
172
- mime_type = "text/plain"
173
- extension = "txt"
174
-
175
- _file = Base64File(
176
- url=url,
177
- base64_url= f"data:{mime_type};base64,{base64_content}",
178
- base64_content=base64_content,
179
- name=name,
180
- extension=extension,
181
- mime_type=mime_type
182
- )
183
- if not _error:
184
- await to_cache(_file, url)
185
- return _file
1
+ from typing import List,Optional
2
+ import os, logging, aiohttp, asyncio, hashlib, json
3
+ import uuid
4
+ from pydantic import BaseModel
5
+ import base64, requests, mimetypes
6
+ from urllib.parse import urlparse
7
+ from tqdm.asyncio import tqdm
8
+ from ws_bom_robot_app.config import config
9
+ import aiofiles
10
+
11
+ async def download_files(urls: List[str], destination_folder: str, authorization: str = None):
12
+ tasks = [download_file(file, os.path.join(destination_folder, os.path.basename(file)), authorization=authorization) for file in urls]
13
+ results = await asyncio.gather(*tasks, return_exceptions=False)
14
+ for i, result in enumerate(results):
15
+ if not result:
16
+ raise Exception(f"Download failed for file: {urls[i]}")
17
+
18
+ async def download_file(url: str, destination: str, chunk_size: int = 8192, authorization: str = None) -> Optional[str]:
19
+ """
20
+ Downloads a file from a given URL to a destination path asynchronously.
21
+
22
+ Args:
23
+ url: The URL of the file to download
24
+ destination: The local path where the file should be saved
25
+ chunk_size: Size of chunks to download (default: 8192 bytes)
26
+
27
+ Returns:
28
+ str: Path to the downloaded file if successful, None otherwise
29
+
30
+ Raises:
31
+ Various exceptions are caught and logged
32
+ """
33
+ try:
34
+ # Ensure the destination directory exists
35
+ os.makedirs(os.path.dirname(os.path.abspath(destination)), exist_ok=True)
36
+
37
+ async with aiohttp.ClientSession() as session:
38
+ if authorization:
39
+ headers = {'Authorization': authorization}
40
+ session.headers.update(headers)
41
+ async with session.get(url) as response:
42
+ # Check if the request was successful
43
+ if response.status != 200:
44
+ logging.error(f"Failed to download file. Status code: {response.status}")
45
+ return None
46
+
47
+ # Get the total file size if available
48
+ total_size = int(response.headers.get('content-length', 0))
49
+ # Open the destination file and write chunks
50
+ with open(destination, 'wb') as f:
51
+ with tqdm(
52
+ total=total_size,
53
+ desc="Downloading",
54
+ unit='B',
55
+ unit_scale=True,
56
+ unit_divisor=1024
57
+ ) as pbar:
58
+ async for chunk in response.content.iter_chunked(chunk_size):
59
+ if chunk:
60
+ f.write(chunk)
61
+ pbar.update(len(chunk))
62
+
63
+ logging.info(f"File downloaded successfully to {destination}")
64
+ return destination
65
+
66
+ except aiohttp.ClientError as e:
67
+ logging.error(f"Network error occurred: {str(e)}")
68
+ return None
69
+ except asyncio.TimeoutError:
70
+ logging.error("Download timed out")
71
+ return None
72
+ except IOError as e:
73
+ logging.error(f"IO error occurred: {str(e)}")
74
+ return None
75
+ except Exception as e:
76
+ logging.error(f"Unexpected error occurred: {str(e)}")
77
+ return None
78
+ finally:
79
+ # If download failed and file was partially created, clean it up
80
+ if os.path.exists(destination) and os.path.getsize(destination) == 0:
81
+ try:
82
+ os.remove(destination)
83
+ logging.info(f"Cleaned up incomplete download: {destination}")
84
+ except OSError:
85
+ pass
86
+
87
+ class Base64File(BaseModel):
88
+ """Base64 encoded file representation"""
89
+ url: str
90
+ base64_url: str
91
+ base64_content: str
92
+ name: str
93
+ extension: str
94
+ mime_type: str
95
+
96
+ @staticmethod
97
+ def _is_base64_data_uri(url: str) -> bool:
98
+ """Check if URL is already a base64 data URI"""
99
+ return (isinstance(url, str) and
100
+ url.startswith('data:') and
101
+ ';base64,' in url and
102
+ len(url.split(',')) == 2)
103
+
104
+ async def from_url(url: str) -> "Base64File":
105
+ """Download file and return as base64 data URI"""
106
+ def _cache_file(url: str) -> str:
107
+ _hash = hashlib.md5(url.encode()).hexdigest()
108
+ return os.path.join(config.robot_data_folder, config.robot_data_attachment_folder, f"{_hash}.json")
109
+ async def from_cache(url: str) -> "Base64File":
110
+ """Check if file is already downloaded and return data"""
111
+ _file = _cache_file(url)
112
+ if os.path.exists(_file):
113
+ try:
114
+ async with aiofiles.open(_file, 'rb') as f:
115
+ content = await f.read()
116
+ return Base64File(**json.loads(content))
117
+ except Exception as e:
118
+ logging.error(f"Error reading cache file {_file}: {e}")
119
+ return None
120
+ return None
121
+ async def to_cache(file: "Base64File", url: str) -> None:
122
+ """Save file to cache"""
123
+ _file = _cache_file(url)
124
+ try:
125
+ async with aiofiles.open(_file, 'wb') as f:
126
+ await f.write(file.model_dump_json().encode('utf-8'))
127
+ except Exception as e:
128
+ logging.error(f"Error writing cache file {_file}: {e}")
129
+
130
+ # special case: base64 data URI
131
+ if Base64File._is_base64_data_uri(url):
132
+ mime_type = url.split(';')[0].replace('data:', '')
133
+ base64_content = url.split(',')[1]
134
+ extension=mime_type.split('/')[-1]
135
+ name = f"file-{uuid.uuid4()}.{extension}"
136
+ return Base64File(
137
+ url=url,
138
+ base64_url=url,
139
+ base64_content=base64_content,
140
+ name=name,
141
+ extension=extension,
142
+ mime_type=mime_type
143
+ )
144
+
145
+ # default download
146
+ _error = None
147
+ try:
148
+ if _content := await from_cache(url):
149
+ return _content
150
+ async with aiohttp.ClientSession() as session:
151
+ async with session.get(url, timeout=aiohttp.ClientTimeout(total=30), headers={"User-Agent": "Mozilla/5.0"}) as response:
152
+ print(f"Downloading {url} - Status: {response.status}")
153
+ response.raise_for_status()
154
+ content = await response.read()
155
+ # mime type detection
156
+ mime_type = response.headers.get('content-type', '').split(';')[0]
157
+ if not mime_type:
158
+ mime_type, _ = mimetypes.guess_type(urlparse(url).path)
159
+ if not mime_type:
160
+ mime_type = 'application/octet-stream'
161
+ # to base64
162
+ base64_content = base64.b64encode(content).decode('utf-8')
163
+ name = url.split('/')[-1]
164
+ extension = name.split('.')[-1]
165
+ except Exception as e:
166
+ _error = f"Failed to download file from {url}: {e}"
167
+ logging.error(_error)
168
+ base64_content = base64.b64encode(_error.encode('utf-8')).decode('utf-8')
169
+ name = "download_error.txt"
170
+ mime_type = "text/plain"
171
+ extension = "txt"
172
+
173
+ _file = Base64File(
174
+ url=url,
175
+ base64_url= f"data:{mime_type};base64,{base64_content}",
176
+ base64_content=base64_content,
177
+ name=name,
178
+ extension=extension,
179
+ mime_type=mime_type
180
+ )
181
+ if not _error:
182
+ await to_cache(_file, url)
183
+ return _file
@@ -1,29 +1,29 @@
1
- import os, sys, json
2
-
3
- class HiddenPrints:
4
- def __enter__(self):
5
- self._original_stdout = sys.stdout
6
- self._original_stderr = sys.stderr
7
-
8
- sys.stdout = open(os.devnull, 'w')
9
- sys.stderr = open(os.devnull, 'w')
10
-
11
- def __exit__(self, exc_type, exc_val, exc_tb):
12
- sys.stdout.close()
13
- sys.stderr.close()
14
- sys.stdout = self._original_stdout
15
- sys.stderr = self._original_stderr
16
-
17
- def print_json(data) -> str:
18
- return print_single_json(data) + ","
19
-
20
- def print_single_json(data) -> str:
21
- return json.dumps(data, sort_keys=True)
22
-
23
- def print_string(data: str) -> str:
24
- if data != "":
25
- return print_json(data)
26
-
27
- def print_single_string(data: str) -> str:
28
- if data != "":
29
- return print_single_json(data)
1
+ import os, sys, json
2
+
3
+ class HiddenPrints:
4
+ def __enter__(self):
5
+ self._original_stdout = sys.stdout
6
+ self._original_stderr = sys.stderr
7
+
8
+ sys.stdout = open(os.devnull, 'w')
9
+ sys.stderr = open(os.devnull, 'w')
10
+
11
+ def __exit__(self, exc_type, exc_val, exc_tb):
12
+ sys.stdout.close()
13
+ sys.stderr.close()
14
+ sys.stdout = self._original_stdout
15
+ sys.stderr = self._original_stderr
16
+
17
+ def print_json(data) -> str:
18
+ return print_single_json(data) + ","
19
+
20
+ def print_single_json(data) -> str:
21
+ return json.dumps(data, sort_keys=True)
22
+
23
+ def print_string(data: str) -> str:
24
+ if data != "":
25
+ return print_json(data)
26
+
27
+ def print_single_string(data: str) -> str:
28
+ if data != "":
29
+ return print_single_json(data)