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.
- mfcli/.env.example +72 -0
- mfcli/__init__.py +0 -0
- mfcli/agents/__init__.py +0 -0
- mfcli/agents/controller/__init__.py +0 -0
- mfcli/agents/controller/agent.py +19 -0
- mfcli/agents/controller/config.yaml +27 -0
- mfcli/agents/controller/tools.py +42 -0
- mfcli/agents/tools/general.py +118 -0
- mfcli/alembic/env.py +61 -0
- mfcli/alembic/script.py.mako +28 -0
- mfcli/alembic/versions/6ccc0c7c397c_added_fields_to_pdf_parts_model.py +39 -0
- mfcli/alembic/versions/769019ef4870_added_gemini_file_path_to_pdf_part_model.py +33 -0
- mfcli/alembic/versions/7a2e3a779fdc_added_functional_block_and_component_.py +54 -0
- mfcli/alembic/versions/7d5adb2a47a7_added_pdf_parts_model.py +41 -0
- mfcli/alembic/versions/7fcb7d6a5836_init.py +167 -0
- mfcli/alembic/versions/e0f2b5765c72_added_cascade_delete_for_models_that_.py +32 -0
- mfcli/alembic.ini +147 -0
- mfcli/cli/__init__.py +0 -0
- mfcli/cli/dependencies.py +59 -0
- mfcli/cli/main.py +192 -0
- mfcli/client/__init__.py +0 -0
- mfcli/client/chroma_db.py +184 -0
- mfcli/client/docling.py +44 -0
- mfcli/client/gemini.py +252 -0
- mfcli/client/llama_parse.py +38 -0
- mfcli/client/vector_db.py +93 -0
- mfcli/constants/__init__.py +0 -0
- mfcli/constants/base_enum.py +18 -0
- mfcli/constants/directory_names.py +1 -0
- mfcli/constants/file_types.py +189 -0
- mfcli/constants/gemini.py +1 -0
- mfcli/constants/openai.py +6 -0
- mfcli/constants/pipeline_run_status.py +3 -0
- mfcli/crud/__init__.py +0 -0
- mfcli/crud/file.py +42 -0
- mfcli/crud/functional_blocks.py +26 -0
- mfcli/crud/netlist.py +18 -0
- mfcli/crud/pipeline_run.py +17 -0
- mfcli/crud/project.py +99 -0
- mfcli/digikey/__init__.py +0 -0
- mfcli/digikey/digikey.py +105 -0
- mfcli/main.py +5 -0
- mfcli/mcp/__init__.py +0 -0
- mfcli/mcp/configs/cline_mcp_settings.json +11 -0
- mfcli/mcp/configs/mfcli.mcp.json +7 -0
- mfcli/mcp/mcp_instance.py +6 -0
- mfcli/mcp/server.py +37 -0
- mfcli/mcp/state_manager.py +51 -0
- mfcli/mcp/tools/__init__.py +0 -0
- mfcli/mcp/tools/query_knowledgebase.py +108 -0
- mfcli/models/__init__.py +10 -0
- mfcli/models/base.py +10 -0
- mfcli/models/bom.py +71 -0
- mfcli/models/datasheet.py +10 -0
- mfcli/models/debug_setup.py +64 -0
- mfcli/models/file.py +43 -0
- mfcli/models/file_docket.py +94 -0
- mfcli/models/file_metadata.py +19 -0
- mfcli/models/functional_blocks.py +94 -0
- mfcli/models/llm_response.py +5 -0
- mfcli/models/mcu.py +97 -0
- mfcli/models/mcu_errata.py +26 -0
- mfcli/models/netlist.py +59 -0
- mfcli/models/pdf_parts.py +25 -0
- mfcli/models/pipeline_run.py +34 -0
- mfcli/models/project.py +27 -0
- mfcli/models/project_metadata.py +15 -0
- mfcli/pipeline/__init__.py +0 -0
- mfcli/pipeline/analysis/__init__.py +0 -0
- mfcli/pipeline/analysis/bom_netlist_mapper.py +28 -0
- mfcli/pipeline/analysis/generators/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/bom/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/bom/bom.py +74 -0
- mfcli/pipeline/analysis/generators/debug_setup/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/debug_setup/debug_setup.py +71 -0
- mfcli/pipeline/analysis/generators/debug_setup/instructions.py +150 -0
- mfcli/pipeline/analysis/generators/functional_blocks/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/functional_blocks/functional_blocks.py +93 -0
- mfcli/pipeline/analysis/generators/functional_blocks/instructions.py +34 -0
- mfcli/pipeline/analysis/generators/functional_blocks/validator.py +94 -0
- mfcli/pipeline/analysis/generators/generator.py +258 -0
- mfcli/pipeline/analysis/generators/generator_base.py +18 -0
- mfcli/pipeline/analysis/generators/mcu/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/mcu/instructions.py +156 -0
- mfcli/pipeline/analysis/generators/mcu/mcu.py +84 -0
- mfcli/pipeline/analysis/generators/mcu_errata/__init__.py +1 -0
- mfcli/pipeline/analysis/generators/mcu_errata/instructions.py +77 -0
- mfcli/pipeline/analysis/generators/mcu_errata/mcu_errata.py +95 -0
- mfcli/pipeline/analysis/generators/summary/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/summary/summary.py +47 -0
- mfcli/pipeline/classifier.py +93 -0
- mfcli/pipeline/data_enricher.py +15 -0
- mfcli/pipeline/extractor.py +34 -0
- mfcli/pipeline/extractors/__init__.py +0 -0
- mfcli/pipeline/extractors/pdf.py +12 -0
- mfcli/pipeline/parser.py +120 -0
- mfcli/pipeline/parsers/__init__.py +0 -0
- mfcli/pipeline/parsers/netlist/__init__.py +0 -0
- mfcli/pipeline/parsers/netlist/edif.py +93 -0
- mfcli/pipeline/parsers/netlist/kicad_legacy_net.py +326 -0
- mfcli/pipeline/parsers/netlist/kicad_spice.py +135 -0
- mfcli/pipeline/parsers/netlist/pads.py +185 -0
- mfcli/pipeline/parsers/netlist/protel.py +166 -0
- mfcli/pipeline/parsers/netlist/protel_detector.py +29 -0
- mfcli/pipeline/pipeline.py +419 -0
- mfcli/pipeline/preprocessors/__init__.py +0 -0
- mfcli/pipeline/preprocessors/user_guide.py +127 -0
- mfcli/pipeline/run_context.py +32 -0
- mfcli/pipeline/schema_mapper.py +89 -0
- mfcli/pipeline/sub_classifier.py +115 -0
- mfcli/utils/__init__.py +0 -0
- mfcli/utils/config.py +33 -0
- mfcli/utils/configurator.py +324 -0
- mfcli/utils/data_cleaner.py +82 -0
- mfcli/utils/datasheet_vectorizer.py +281 -0
- mfcli/utils/directory_manager.py +96 -0
- mfcli/utils/file_upload.py +298 -0
- mfcli/utils/files.py +16 -0
- mfcli/utils/http_requests.py +54 -0
- mfcli/utils/kb_lister.py +89 -0
- mfcli/utils/kb_remover.py +173 -0
- mfcli/utils/logger.py +28 -0
- mfcli/utils/mcp_configurator.py +311 -0
- mfcli/utils/migrations.py +18 -0
- mfcli/utils/orm.py +43 -0
- mfcli/utils/pdf_splitter.py +63 -0
- mfcli/utils/query_service.py +22 -0
- mfcli/utils/system_check.py +306 -0
- mfcli/utils/tools.py +31 -0
- mfcli/utils/vectorizer.py +28 -0
- mfcli-0.2.0.dist-info/METADATA +841 -0
- mfcli-0.2.0.dist-info/RECORD +136 -0
- mfcli-0.2.0.dist-info/WHEEL +5 -0
- mfcli-0.2.0.dist-info/entry_points.txt +3 -0
- mfcli-0.2.0.dist-info/licenses/LICENSE +21 -0
- 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()
|
mfcli/utils/kb_lister.py
ADDED
|
@@ -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
|