mfcli 0.2.0__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 (136) hide show
  1. mfcli/.env.example +72 -0
  2. mfcli/__init__.py +0 -0
  3. mfcli/agents/__init__.py +0 -0
  4. mfcli/agents/controller/__init__.py +0 -0
  5. mfcli/agents/controller/agent.py +19 -0
  6. mfcli/agents/controller/config.yaml +27 -0
  7. mfcli/agents/controller/tools.py +42 -0
  8. mfcli/agents/tools/general.py +118 -0
  9. mfcli/alembic/env.py +61 -0
  10. mfcli/alembic/script.py.mako +28 -0
  11. mfcli/alembic/versions/6ccc0c7c397c_added_fields_to_pdf_parts_model.py +39 -0
  12. mfcli/alembic/versions/769019ef4870_added_gemini_file_path_to_pdf_part_model.py +33 -0
  13. mfcli/alembic/versions/7a2e3a779fdc_added_functional_block_and_component_.py +54 -0
  14. mfcli/alembic/versions/7d5adb2a47a7_added_pdf_parts_model.py +41 -0
  15. mfcli/alembic/versions/7fcb7d6a5836_init.py +167 -0
  16. mfcli/alembic/versions/e0f2b5765c72_added_cascade_delete_for_models_that_.py +32 -0
  17. mfcli/alembic.ini +147 -0
  18. mfcli/cli/__init__.py +0 -0
  19. mfcli/cli/dependencies.py +59 -0
  20. mfcli/cli/main.py +192 -0
  21. mfcli/client/__init__.py +0 -0
  22. mfcli/client/chroma_db.py +184 -0
  23. mfcli/client/docling.py +44 -0
  24. mfcli/client/gemini.py +252 -0
  25. mfcli/client/llama_parse.py +38 -0
  26. mfcli/client/vector_db.py +93 -0
  27. mfcli/constants/__init__.py +0 -0
  28. mfcli/constants/base_enum.py +18 -0
  29. mfcli/constants/directory_names.py +1 -0
  30. mfcli/constants/file_types.py +189 -0
  31. mfcli/constants/gemini.py +1 -0
  32. mfcli/constants/openai.py +6 -0
  33. mfcli/constants/pipeline_run_status.py +3 -0
  34. mfcli/crud/__init__.py +0 -0
  35. mfcli/crud/file.py +42 -0
  36. mfcli/crud/functional_blocks.py +26 -0
  37. mfcli/crud/netlist.py +18 -0
  38. mfcli/crud/pipeline_run.py +17 -0
  39. mfcli/crud/project.py +99 -0
  40. mfcli/digikey/__init__.py +0 -0
  41. mfcli/digikey/digikey.py +105 -0
  42. mfcli/main.py +5 -0
  43. mfcli/mcp/__init__.py +0 -0
  44. mfcli/mcp/configs/cline_mcp_settings.json +11 -0
  45. mfcli/mcp/configs/mfcli.mcp.json +7 -0
  46. mfcli/mcp/mcp_instance.py +6 -0
  47. mfcli/mcp/server.py +37 -0
  48. mfcli/mcp/state_manager.py +51 -0
  49. mfcli/mcp/tools/__init__.py +0 -0
  50. mfcli/mcp/tools/query_knowledgebase.py +108 -0
  51. mfcli/models/__init__.py +10 -0
  52. mfcli/models/base.py +10 -0
  53. mfcli/models/bom.py +71 -0
  54. mfcli/models/datasheet.py +10 -0
  55. mfcli/models/debug_setup.py +64 -0
  56. mfcli/models/file.py +43 -0
  57. mfcli/models/file_docket.py +94 -0
  58. mfcli/models/file_metadata.py +19 -0
  59. mfcli/models/functional_blocks.py +94 -0
  60. mfcli/models/llm_response.py +5 -0
  61. mfcli/models/mcu.py +97 -0
  62. mfcli/models/mcu_errata.py +26 -0
  63. mfcli/models/netlist.py +59 -0
  64. mfcli/models/pdf_parts.py +25 -0
  65. mfcli/models/pipeline_run.py +34 -0
  66. mfcli/models/project.py +27 -0
  67. mfcli/models/project_metadata.py +15 -0
  68. mfcli/pipeline/__init__.py +0 -0
  69. mfcli/pipeline/analysis/__init__.py +0 -0
  70. mfcli/pipeline/analysis/bom_netlist_mapper.py +28 -0
  71. mfcli/pipeline/analysis/generators/__init__.py +0 -0
  72. mfcli/pipeline/analysis/generators/bom/__init__.py +0 -0
  73. mfcli/pipeline/analysis/generators/bom/bom.py +74 -0
  74. mfcli/pipeline/analysis/generators/debug_setup/__init__.py +0 -0
  75. mfcli/pipeline/analysis/generators/debug_setup/debug_setup.py +71 -0
  76. mfcli/pipeline/analysis/generators/debug_setup/instructions.py +150 -0
  77. mfcli/pipeline/analysis/generators/functional_blocks/__init__.py +0 -0
  78. mfcli/pipeline/analysis/generators/functional_blocks/functional_blocks.py +93 -0
  79. mfcli/pipeline/analysis/generators/functional_blocks/instructions.py +34 -0
  80. mfcli/pipeline/analysis/generators/functional_blocks/validator.py +94 -0
  81. mfcli/pipeline/analysis/generators/generator.py +258 -0
  82. mfcli/pipeline/analysis/generators/generator_base.py +18 -0
  83. mfcli/pipeline/analysis/generators/mcu/__init__.py +0 -0
  84. mfcli/pipeline/analysis/generators/mcu/instructions.py +156 -0
  85. mfcli/pipeline/analysis/generators/mcu/mcu.py +84 -0
  86. mfcli/pipeline/analysis/generators/mcu_errata/__init__.py +1 -0
  87. mfcli/pipeline/analysis/generators/mcu_errata/instructions.py +77 -0
  88. mfcli/pipeline/analysis/generators/mcu_errata/mcu_errata.py +95 -0
  89. mfcli/pipeline/analysis/generators/summary/__init__.py +0 -0
  90. mfcli/pipeline/analysis/generators/summary/summary.py +47 -0
  91. mfcli/pipeline/classifier.py +93 -0
  92. mfcli/pipeline/data_enricher.py +15 -0
  93. mfcli/pipeline/extractor.py +34 -0
  94. mfcli/pipeline/extractors/__init__.py +0 -0
  95. mfcli/pipeline/extractors/pdf.py +12 -0
  96. mfcli/pipeline/parser.py +120 -0
  97. mfcli/pipeline/parsers/__init__.py +0 -0
  98. mfcli/pipeline/parsers/netlist/__init__.py +0 -0
  99. mfcli/pipeline/parsers/netlist/edif.py +93 -0
  100. mfcli/pipeline/parsers/netlist/kicad_legacy_net.py +326 -0
  101. mfcli/pipeline/parsers/netlist/kicad_spice.py +135 -0
  102. mfcli/pipeline/parsers/netlist/pads.py +185 -0
  103. mfcli/pipeline/parsers/netlist/protel.py +166 -0
  104. mfcli/pipeline/parsers/netlist/protel_detector.py +29 -0
  105. mfcli/pipeline/pipeline.py +419 -0
  106. mfcli/pipeline/preprocessors/__init__.py +0 -0
  107. mfcli/pipeline/preprocessors/user_guide.py +127 -0
  108. mfcli/pipeline/run_context.py +32 -0
  109. mfcli/pipeline/schema_mapper.py +89 -0
  110. mfcli/pipeline/sub_classifier.py +115 -0
  111. mfcli/utils/__init__.py +0 -0
  112. mfcli/utils/config.py +33 -0
  113. mfcli/utils/configurator.py +324 -0
  114. mfcli/utils/data_cleaner.py +82 -0
  115. mfcli/utils/datasheet_vectorizer.py +281 -0
  116. mfcli/utils/directory_manager.py +96 -0
  117. mfcli/utils/file_upload.py +298 -0
  118. mfcli/utils/files.py +16 -0
  119. mfcli/utils/http_requests.py +54 -0
  120. mfcli/utils/kb_lister.py +89 -0
  121. mfcli/utils/kb_remover.py +173 -0
  122. mfcli/utils/logger.py +28 -0
  123. mfcli/utils/mcp_configurator.py +311 -0
  124. mfcli/utils/migrations.py +18 -0
  125. mfcli/utils/orm.py +43 -0
  126. mfcli/utils/pdf_splitter.py +63 -0
  127. mfcli/utils/query_service.py +22 -0
  128. mfcli/utils/system_check.py +306 -0
  129. mfcli/utils/tools.py +31 -0
  130. mfcli/utils/vectorizer.py +28 -0
  131. mfcli-0.2.0.dist-info/METADATA +841 -0
  132. mfcli-0.2.0.dist-info/RECORD +136 -0
  133. mfcli-0.2.0.dist-info/WHEEL +5 -0
  134. mfcli-0.2.0.dist-info/entry_points.txt +3 -0
  135. mfcli-0.2.0.dist-info/licenses/LICENSE +21 -0
  136. mfcli-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,96 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
5
+
6
+ class DirectoryManager:
7
+ _instance = None
8
+
9
+ def __new__(cls):
10
+ if cls._instance is None:
11
+ cls._instance = super().__new__(cls)
12
+ cls._instance._initialized = False
13
+ return cls._instance
14
+
15
+ def __init__(self):
16
+ if self._initialized:
17
+ return
18
+ # OS-specific base appdata location
19
+ if os.name == "nt":
20
+ app_data_base = Path(os.getenv("LOCALAPPDATA", os.getenv("APPDATA")))
21
+ elif sys.platform == "darwin":
22
+ app_data_base = Path.home() / "Library" / "Application Support"
23
+ else:
24
+ app_data_base = Path.home() / ".local" / "share"
25
+
26
+ self.home_dir: Path = Path(os.path.expanduser("~")) / "Multifactor"
27
+ self.env_file_path: Path = self.home_dir / ".env"
28
+
29
+ # User app directories
30
+ self.app_data_dir: Path = app_data_base / "Multifactor"
31
+ self.chroma_db_dir: Path = self.app_data_dir / "chromadb"
32
+
33
+ self.app_data_dir.mkdir(exist_ok=True, parents=True)
34
+ self.chroma_db_dir.mkdir(exist_ok=True, parents=True)
35
+ self.home_dir.mkdir(exist_ok=True, parents=True)
36
+
37
+ # Repo dirs
38
+ self.root_dir: Path | None = None
39
+ self.agent_instructions_dir: Path | None = None
40
+ self.data_sheets_dir: Path | None = None
41
+ self.fw_tasks_dir: Path | None = None
42
+ self.generated_files_dir: Path | None = None
43
+ self.cheat_sheets_dir: Path | None = None
44
+ self.reqs_dir: Path | None = None
45
+ self.pdf_parts_dir: Path | None = None
46
+ self.metadata_dir: Path | None = None
47
+ self.config_file_path: Path | None = None
48
+ self.file_docket_path: Path | None = None
49
+
50
+ self._initialized = True
51
+
52
+ def initialize(self, root: str):
53
+ # Accept file or directory
54
+ root_path = Path(root)
55
+ if root_path.is_file():
56
+ self.root_dir = root_path.parent
57
+ else:
58
+ self.root_dir = root_path
59
+
60
+ # Repo directories
61
+ repo_dir = self.root_dir.parent
62
+ self.agent_instructions_dir = repo_dir / "agent_instructions"
63
+ self.data_sheets_dir = repo_dir / "data_sheets"
64
+ self.fw_tasks_dir = repo_dir / "fw_tasks"
65
+ self.generated_files_dir = repo_dir / "generated_files"
66
+ self.cheat_sheets_dir = repo_dir / "hw_cheat_sheets"
67
+ self.reqs_dir = repo_dir / "requirements"
68
+ self.pdf_parts_dir = repo_dir / "pdf_parts"
69
+ self.metadata_dir = self.root_dir / ".multifactor"
70
+ self.config_file_path = self.metadata_dir / "config.json"
71
+ self.file_docket_path = self.metadata_dir / "file_docket.json"
72
+
73
+ # Create all dirs
74
+ self._create_directory_structure()
75
+
76
+ def _create_directory_structure(self):
77
+ for directory in [
78
+ self.agent_instructions_dir,
79
+ self.data_sheets_dir,
80
+ self.fw_tasks_dir,
81
+ self.generated_files_dir,
82
+ self.cheat_sheets_dir,
83
+ self.reqs_dir,
84
+ self.pdf_parts_dir,
85
+ self.app_data_dir,
86
+ self.chroma_db_dir,
87
+ self.metadata_dir
88
+ ]:
89
+ directory.mkdir(parents=True, exist_ok=True)
90
+
91
+
92
+ app_dirs = DirectoryManager()
93
+
94
+
95
+ def init_directory_structure(root_dir: str):
96
+ app_dirs.initialize(root_dir)
@@ -0,0 +1,298 @@
1
+ """Unified file upload abstraction for different LLM providers."""
2
+
3
+ import os
4
+ from abc import ABC, abstractmethod
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from google import genai
10
+ from google.genai.types import File
11
+
12
+ from mfcli.utils.config import get_config
13
+ from mfcli.utils.logger import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class FileUploadProvider(str, Enum):
19
+ """Supported file upload providers."""
20
+ GEMINI = "gemini"
21
+ OPENAI = "openai"
22
+
23
+
24
+ class BaseFileUploader(ABC):
25
+ """Base class for file upload implementations."""
26
+
27
+ @abstractmethod
28
+ def upload_file(self, file_path: str, display_name: Optional[str] = None) -> dict:
29
+ """
30
+ Upload a file to the provider's file storage.
31
+
32
+ Args:
33
+ file_path: Path to the local file to upload
34
+ display_name: Optional display name for the file
35
+
36
+ Returns:
37
+ Dictionary containing file metadata including URI/ID for accessing the file
38
+ """
39
+ pass
40
+
41
+ @abstractmethod
42
+ def delete_file(self, file_id: str) -> bool:
43
+ """
44
+ Delete a file from the provider's storage.
45
+
46
+ Args:
47
+ file_id: The ID/URI of the file to delete
48
+
49
+ Returns:
50
+ True if deletion was successful, False otherwise
51
+ """
52
+ pass
53
+
54
+ @abstractmethod
55
+ def get_file_info(self, file_id: str) -> dict:
56
+ """
57
+ Get information about an uploaded file.
58
+
59
+ Args:
60
+ file_id: The ID/URI of the file
61
+
62
+ Returns:
63
+ Dictionary containing file metadata
64
+ """
65
+ pass
66
+
67
+
68
+ class GeminiFileUploader(BaseFileUploader):
69
+ """File uploader implementation for Google Gemini."""
70
+
71
+ def __init__(self):
72
+ """Initialize the Gemini file uploader."""
73
+ config = get_config()
74
+ self.client = genai.Client(api_key=config.google_api_key)
75
+ logger.info("Initialized Gemini file uploader")
76
+
77
+ @staticmethod
78
+ def _file_access_check(file_path: str):
79
+ file_path_obj = Path(file_path)
80
+
81
+ # Validate file exists and is readable
82
+ if not file_path_obj.exists():
83
+ raise ValueError(f"File does not exist: {file_path}")
84
+ if not os.access(file_path_obj, os.R_OK):
85
+ raise ValueError(f"File is not readable: {file_path}")
86
+
87
+ def upload(self, file_path: str) -> File:
88
+ """
89
+ Upload a file to Gemini Files API and return File object.
90
+
91
+ Args:
92
+ file_path: Path to the local file to upload
93
+
94
+ Returns:
95
+ Gemini types File object.
96
+
97
+ Raises:
98
+ ValueError: If file doesn't exist or is not readable
99
+ Exception: If upload fails
100
+ """
101
+ self._file_access_check(file_path)
102
+ return self.client.files.upload(file=file_path)
103
+
104
+ def upload_file(self, file_path: str, display_name: Optional[str] = None) -> dict:
105
+ """
106
+ Upload a file to Gemini Files API.
107
+
108
+ Args:
109
+ file_path: Path to the local file to upload
110
+ display_name: Optional display name for the file
111
+
112
+ Returns:
113
+ Dictionary with file metadata including 'uri', 'name', 'mime_type', 'size_bytes'
114
+
115
+ Raises:
116
+ ValueError: If file doesn't exist or is not readable
117
+ Exception: If upload fails
118
+ """
119
+
120
+ self._file_access_check(file_path)
121
+
122
+ file_path_obj = Path(file_path)
123
+
124
+ # Use filename as display name if not provided
125
+ if display_name is None:
126
+ display_name = file_path_obj.name
127
+
128
+ try:
129
+ logger.info(f"Uploading file to Gemini: {file_path}")
130
+
131
+ # Upload the file
132
+ uploaded_file = self.client.files.upload(
133
+ file=str(file_path_obj),
134
+ config={'display_name': display_name}
135
+ )
136
+
137
+ # Extract metadata
138
+ result = {
139
+ 'uri': uploaded_file.uri,
140
+ 'name': uploaded_file.name,
141
+ 'display_name': uploaded_file.display_name,
142
+ 'mime_type': uploaded_file.mime_type,
143
+ 'size_bytes': uploaded_file.size_bytes,
144
+ 'state': uploaded_file.state.name,
145
+ 'provider': FileUploadProvider.GEMINI.value
146
+ }
147
+
148
+ logger.info(f"Successfully uploaded file: {result['name']}")
149
+ return result
150
+
151
+ except Exception as e:
152
+ logger.error(f"Failed to upload file to Gemini: {e}")
153
+ raise Exception(f"Failed to upload file to Gemini: {str(e)}")
154
+
155
+ def delete_file(self, file_id: str) -> bool:
156
+ """
157
+ Delete a file from Gemini Files API.
158
+
159
+ Args:
160
+ file_id: The name/ID of the file (e.g., 'files/abc123')
161
+
162
+ Returns:
163
+ True if deletion was successful, False otherwise
164
+ """
165
+ try:
166
+ logger.info(f"Deleting file from Gemini: {file_id}")
167
+ self.client.files.delete(name=file_id)
168
+ logger.info(f"Successfully deleted file: {file_id}")
169
+ return True
170
+ except Exception as e:
171
+ logger.error(f"Failed to delete file from Gemini: {e}")
172
+ return False
173
+
174
+ def get_file_info(self, file_id: str) -> dict:
175
+ """
176
+ Get information about an uploaded file.
177
+
178
+ Args:
179
+ file_id: The name/ID of the file (e.g., 'files/abc123')
180
+
181
+ Returns:
182
+ Dictionary containing file metadata
183
+ """
184
+ try:
185
+ logger.info(f"Getting file info from Gemini: {file_id}")
186
+ file_info = self.client.files.get(name=file_id)
187
+
188
+ result = {
189
+ 'uri': file_info.uri,
190
+ 'name': file_info.name,
191
+ 'display_name': file_info.display_name,
192
+ 'mime_type': file_info.mime_type,
193
+ 'size_bytes': file_info.size_bytes,
194
+ 'state': file_info.state.name,
195
+ 'provider': FileUploadProvider.GEMINI.value
196
+ }
197
+
198
+ return result
199
+
200
+ except Exception as e:
201
+ logger.error(f"Failed to get file info from Gemini: {e}")
202
+ raise Exception(f"Failed to get file info: {str(e)}")
203
+
204
+
205
+ class OpenAIFileUploader(BaseFileUploader):
206
+ """File uploader implementation for OpenAI (placeholder for future implementation)."""
207
+
208
+ def __init__(self):
209
+ """Initialize the OpenAI file uploader."""
210
+ config = get_config()
211
+ # This will be implemented when OpenAI support is added
212
+ logger.info("OpenAI file uploader - not yet implemented")
213
+ raise NotImplementedError("OpenAI file upload support coming soon")
214
+
215
+ def upload_file(self, file_path: str, display_name: Optional[str] = None) -> dict:
216
+ """Upload a file to OpenAI."""
217
+ raise NotImplementedError("OpenAI file upload not yet implemented")
218
+
219
+ def delete_file(self, file_id: str) -> bool:
220
+ """Delete a file from OpenAI."""
221
+ raise NotImplementedError("OpenAI file deletion not yet implemented")
222
+
223
+ def get_file_info(self, file_id: str) -> dict:
224
+ """Get file info from OpenAI."""
225
+ raise NotImplementedError("OpenAI file info not yet implemented")
226
+
227
+
228
+ class FileUploadManager:
229
+ """Manager class to handle file uploads across different providers."""
230
+
231
+ def __init__(self, provider: FileUploadProvider = FileUploadProvider.GEMINI):
232
+ """
233
+ Initialize the file upload manager.
234
+
235
+ Args:
236
+ provider: The file upload provider to use (default: GEMINI)
237
+ """
238
+ self.provider = provider
239
+ self.uploader = self._get_uploader(provider)
240
+
241
+ def _get_uploader(self, provider: FileUploadProvider) -> BaseFileUploader:
242
+ """Get the appropriate uploader for the specified provider."""
243
+ if provider == FileUploadProvider.GEMINI:
244
+ return GeminiFileUploader()
245
+ elif provider == FileUploadProvider.OPENAI:
246
+ return OpenAIFileUploader()
247
+ else:
248
+ raise ValueError(f"Unsupported file upload provider: {provider}")
249
+
250
+ def upload_file(self, file_path: str, display_name: Optional[str] = None) -> dict:
251
+ """
252
+ Upload a file using the configured provider.
253
+
254
+ Args:
255
+ file_path: Path to the local file to upload
256
+ display_name: Optional display name for the file
257
+
258
+ Returns:
259
+ Dictionary with file metadata
260
+ """
261
+ return self.uploader.upload_file(file_path, display_name)
262
+
263
+ def delete_file(self, file_id: str) -> bool:
264
+ """
265
+ Delete a file using the configured provider.
266
+
267
+ Args:
268
+ file_id: The ID/URI of the file to delete
269
+
270
+ Returns:
271
+ True if deletion was successful, False otherwise
272
+ """
273
+ return self.uploader.delete_file(file_id)
274
+
275
+ def get_file_info(self, file_id: str) -> dict:
276
+ """
277
+ Get file information using the configured provider.
278
+
279
+ Args:
280
+ file_id: The ID/URI of the file
281
+
282
+ Returns:
283
+ Dictionary containing file metadata
284
+ """
285
+ return self.uploader.get_file_info(file_id)
286
+
287
+
288
+ def get_file_upload_manager(provider: FileUploadProvider = FileUploadProvider.GEMINI) -> FileUploadManager:
289
+ """
290
+ Factory function to get a file upload manager instance.
291
+
292
+ Args:
293
+ provider: The file upload provider to use (default: GEMINI)
294
+
295
+ Returns:
296
+ FileUploadManager instance
297
+ """
298
+ return FileUploadManager(provider)
mfcli/utils/files.py ADDED
@@ -0,0 +1,16 @@
1
+ import os
2
+ import re
3
+ from pathlib import Path
4
+ from typing import Literal
5
+
6
+ AppDataFileTypes = Literal['datasheets', 'files']
7
+
8
+
9
+ def file_access_check(file_path: Path) -> bool:
10
+ if not os.path.exists(file_path) or not os.access(file_path, os.R_OK):
11
+ return False
12
+ return True
13
+
14
+
15
+ def is_text_mime_type(mime_type: str) -> bool:
16
+ return bool(re.match(r"^text/.+$", mime_type))
@@ -0,0 +1,54 @@
1
+ import requests
2
+ from requests.adapters import HTTPAdapter
3
+ from urllib3.util.retry import Retry
4
+
5
+
6
+ def get_retry_session(
7
+ retries: int = 3,
8
+ backoff_factor: float = 0.5,
9
+ status_forcelist: tuple = (500, 502, 503, 504),
10
+ allowed_methods: tuple = ("GET", "POST", "PUT", "DELETE", "PATCH")
11
+ ) -> requests.Session:
12
+ """Create a requests session with retry logic."""
13
+ session = requests.Session()
14
+ retry = Retry(
15
+ total=retries,
16
+ read=retries,
17
+ connect=retries,
18
+ backoff_factor=backoff_factor,
19
+ status_forcelist=status_forcelist,
20
+ allowed_methods=allowed_methods,
21
+ raise_on_status=False,
22
+ )
23
+ adapter = HTTPAdapter(max_retries=retry)
24
+ session.mount("http://", adapter)
25
+ session.mount("https://", adapter)
26
+ return session
27
+
28
+
29
+ def http_request(
30
+ method: str,
31
+ url: str,
32
+ retries: int = 3,
33
+ timeout: int = 10,
34
+ **kwargs
35
+ ) -> requests.Response:
36
+ """
37
+ Perform an HTTP request with automatic retry logic.
38
+
39
+ :param method: HTTP method (GET, POST, PUT, DELETE, etc.)
40
+ :param url: URL to request
41
+ :param retries: Number of retry attempts
42
+ :param timeout: Timeout per request in seconds
43
+ :param kwargs: Any requests.request() parameters (headers, data, json, etc.)
44
+ :return: requests.Response object
45
+ """
46
+ session = get_retry_session(retries=retries)
47
+ try:
48
+ response = session.request(method.upper(), url, allow_redirects=True, timeout=timeout, **kwargs)
49
+ response.raise_for_status()
50
+ return response
51
+ except requests.exceptions.RequestException:
52
+ raise
53
+ finally:
54
+ session.close()
@@ -0,0 +1,89 @@
1
+ from typing import Dict, List, Set
2
+ from mfcli.client.chroma_db import get_chromadb_client_for_project_name
3
+ from mfcli.utils.logger import get_logger
4
+ from mfcli.utils.orm import Session
5
+
6
+ logger = get_logger(__name__)
7
+
8
+
9
+ def list_vectorized_files(project_name: str) -> Dict[str, List[str]]:
10
+ """
11
+ List all files that have been vectorized into the ChromaDB database for a project.
12
+
13
+ Args:
14
+ project_name: Name of the project
15
+
16
+ Returns:
17
+ Dictionary mapping purpose to list of file names
18
+ """
19
+ try:
20
+ with Session() as db:
21
+ chroma_client = get_chromadb_client_for_project_name(db, project_name)
22
+
23
+ # Get all documents from the collection
24
+ collection = chroma_client._collection
25
+ results = collection.get()
26
+
27
+ if not results or not results.get('metadatas'):
28
+ logger.info("No vectorized files found in the knowledge base")
29
+ return {}
30
+
31
+ # Group files by purpose
32
+ files_by_purpose: Dict[str, Set[str]] = {}
33
+
34
+ for metadata in results['metadatas']:
35
+ if metadata:
36
+ file_name = metadata.get('file_name', 'Unknown')
37
+ purpose = metadata.get('purpose', 'unknown')
38
+
39
+ if purpose not in files_by_purpose:
40
+ files_by_purpose[purpose] = set()
41
+ files_by_purpose[purpose].add(file_name)
42
+
43
+ # Convert sets to sorted lists for consistent output
44
+ result = {
45
+ purpose: sorted(list(files))
46
+ for purpose, files in files_by_purpose.items()
47
+ }
48
+
49
+ return result
50
+
51
+ except Exception as e:
52
+ logger.error(f"Failed to list vectorized files for project: {project_name}")
53
+ logger.exception(e)
54
+ raise
55
+
56
+
57
+ def print_vectorized_files(project_name: str) -> None:
58
+ """
59
+ Print all vectorized files in a formatted manner.
60
+
61
+ Args:
62
+ project_name: Name of the project
63
+ """
64
+ try:
65
+ files_by_purpose = list_vectorized_files(project_name)
66
+
67
+ if not files_by_purpose:
68
+ print(f"\nNo files have been vectorized for project '{project_name}' yet.")
69
+ return
70
+
71
+ print(f"\nVectorized files in knowledge base for project '{project_name}':")
72
+ print("=" * 70)
73
+
74
+ total_files = 0
75
+ for purpose in sorted(files_by_purpose.keys()):
76
+ files = files_by_purpose[purpose]
77
+ print(f"\n{purpose.upper()} ({len(files)} file{'s' if len(files) != 1 else ''}):")
78
+ print("-" * 70)
79
+ for file_name in files:
80
+ print(f" • {file_name}")
81
+ total_files += len(files)
82
+
83
+ print("\n" + "=" * 70)
84
+ print(f"Total: {total_files} unique file{'s' if total_files != 1 else ''} vectorized\n")
85
+
86
+ except Exception as e:
87
+ logger.error(f"Failed to print vectorized files for project: {project_name}")
88
+ print(f"\nError: Failed to list vectorized files. Check logs for details.")
89
+ raise