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,189 @@
|
|
|
1
|
+
from typing import Literal, Dict
|
|
2
|
+
|
|
3
|
+
from mfcli.constants.base_enum import BaseEnum
|
|
4
|
+
|
|
5
|
+
FILE_SUBTYPE_UNKNOWN = "UNKNOWN"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FileTypes(BaseEnum):
|
|
9
|
+
CSV = 1
|
|
10
|
+
ASC = 2
|
|
11
|
+
NET = 3
|
|
12
|
+
CIR = 4
|
|
13
|
+
PDF = 5
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileSubtypes(BaseEnum):
|
|
17
|
+
BOM = 1
|
|
18
|
+
PADS_PCB_ASCII = 2
|
|
19
|
+
KICAD_LEGACY_NET = 3
|
|
20
|
+
KICAD_SPICE = 4
|
|
21
|
+
SCHEMATIC = 5
|
|
22
|
+
ERRATA = 6
|
|
23
|
+
MCU_DATASHEET = 7
|
|
24
|
+
PROTEL_ALTIUM = 8
|
|
25
|
+
GENERAL_DATASHEET = 9
|
|
26
|
+
UNKNOWN = 10
|
|
27
|
+
USER_GUIDE = 11
|
|
28
|
+
REFERENCE_MANUAL = 12
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# PDF subtypes which require summary cheat sheets
|
|
32
|
+
SummaryCheatSheetSubtypes: set[FileSubtypes] = {
|
|
33
|
+
FileSubtypes.USER_GUIDE,
|
|
34
|
+
FileSubtypes.REFERENCE_MANUAL
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# PDF file subtypes that do not require docling text extraction or vectorization (example, schematic files)
|
|
38
|
+
PDFNoVectorizeFileSubtypes: set[FileSubtypes] = {
|
|
39
|
+
FileSubtypes.SCHEMATIC
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# File subtypes that have no schema to parse
|
|
43
|
+
SchemalessFileSubtypes: set[FileSubtypes] = {
|
|
44
|
+
FileSubtypes.SCHEMATIC,
|
|
45
|
+
FileSubtypes.ERRATA,
|
|
46
|
+
FileSubtypes.MCU_DATASHEET,
|
|
47
|
+
FileSubtypes.USER_GUIDE,
|
|
48
|
+
FileSubtypes.REFERENCE_MANUAL
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# File subtype names are for use with LLM, validating subtype response
|
|
52
|
+
|
|
53
|
+
PDFFileSubtypeNames = Literal[
|
|
54
|
+
'SCHEMATIC',
|
|
55
|
+
'ERRATA',
|
|
56
|
+
'MCU_DATASHEET',
|
|
57
|
+
'GENERAL_DATASHEET',
|
|
58
|
+
'USER_GUIDE',
|
|
59
|
+
'REFERENCE_MANUAL',
|
|
60
|
+
'UNKNOWN'
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
OtherFileSubtypeNames = Literal[
|
|
64
|
+
'BOM',
|
|
65
|
+
'PADS_PCB_ASCII',
|
|
66
|
+
'KICAD_LEGACY_NET',
|
|
67
|
+
'KICAD_SPICE',
|
|
68
|
+
'PROTEL_ALTIUM'
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
PDFMimeTypes = {
|
|
72
|
+
"application/pdf",
|
|
73
|
+
"application/x-pdf",
|
|
74
|
+
"application/acrobat",
|
|
75
|
+
"applications/vnd.pdf",
|
|
76
|
+
"application/nappdf",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
SupportedFileTypes = {
|
|
80
|
+
"CSV": {
|
|
81
|
+
"mime_types": {
|
|
82
|
+
"text/csv",
|
|
83
|
+
"text/plain"
|
|
84
|
+
},
|
|
85
|
+
"subtypes": {
|
|
86
|
+
"BOM"
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"ASC": {
|
|
90
|
+
"mime_types": {
|
|
91
|
+
"text/plain"
|
|
92
|
+
},
|
|
93
|
+
"subtypes": {
|
|
94
|
+
"PADS_PCB_ASCII"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"NET": {
|
|
98
|
+
"mime_types": {
|
|
99
|
+
"text/plain"
|
|
100
|
+
},
|
|
101
|
+
"subtypes": {
|
|
102
|
+
"KICAD_LEGACY_NET",
|
|
103
|
+
"PROTEL_ALTIUM"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"CIR": {
|
|
107
|
+
"mime_types": {
|
|
108
|
+
"text/plain"
|
|
109
|
+
},
|
|
110
|
+
"subtypes": {
|
|
111
|
+
"KICAD_SPICE"
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"PDF": {
|
|
115
|
+
"mime_types": PDFMimeTypes,
|
|
116
|
+
"subtypes": {
|
|
117
|
+
"SCHEMATIC",
|
|
118
|
+
"ERRATA",
|
|
119
|
+
"MCU_DATASHEET",
|
|
120
|
+
"REFERENCE_MANUAL"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# File subtype descriptions (for use in file subtype auto-discovery)
|
|
126
|
+
|
|
127
|
+
# File subtypes for which we have parsers and do not the LLM to discover subtype
|
|
128
|
+
KnownFileSubtypeDescriptions: Dict[SupportedFileTypes, Dict[str, str]] = {
|
|
129
|
+
"KICAD_LEGACY_NET": {
|
|
130
|
+
"name": 'KiCad Legacy Netlist',
|
|
131
|
+
"description": "Older KiCad netlist format containing components, nets, pins, and footprint references using plain text blocks like (comp ...), (net ...), and (pin ...); does not contain SPICE commands or PCB geometry."
|
|
132
|
+
},
|
|
133
|
+
"PROTEL_ALTIUM": {
|
|
134
|
+
"name": "Protel/Altium Designer Netlist",
|
|
135
|
+
"description": "Hierarchical bracketed netlist with components, footprints, and pin-to-net connections using sections like [Component] and [Net]; verbose structure unique to Altium/Protel; not a PCB layout or SPICE file."
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Only PDF files
|
|
140
|
+
PDFSubtypeDescriptions: Dict[SupportedFileTypes, Dict[str, str]] = {
|
|
141
|
+
"SCHEMATIC": {
|
|
142
|
+
"name": "Schematic",
|
|
143
|
+
"description": "Diagrammatic circuit representation containing symbols, wires, net labels, sheet numbers, and component reference designators; typically PDF or image-based; not a table, netlist, PCB file, or simulation file."
|
|
144
|
+
},
|
|
145
|
+
"ERRATA": {
|
|
146
|
+
"name": "Engineering Errata File",
|
|
147
|
+
"description": "An ERRATA document lists known defects, mistakes, missing features, and silicon bugs in a released product. It is NOT a datasheet, does NOT describe product specifications, and consists of issue-by-issue bullet points with IDs, status, and workarounds. It never contains full electrical characteristics tables, application diagrams, or packaging information."
|
|
148
|
+
},
|
|
149
|
+
"MCU_DATASHEET": {
|
|
150
|
+
"name": "MCU Datasheet",
|
|
151
|
+
"description": "Microcontroller technical reference containing CPU architecture, memory maps, peripheral descriptions, electrical characteristics, pinouts, timing diagrams, and register information; exclusive to microcontrollers."
|
|
152
|
+
},
|
|
153
|
+
"GENERAL_DATASHEET": {
|
|
154
|
+
"name": "General Component Datasheet",
|
|
155
|
+
"description": "Datasheet for non-MCU components such as ICs, sensors, regulators, passives, and connectors, containing electrical specs, pin descriptions, operating conditions, and application circuits; no CPU or register-map content. A general component datasheet is a large, structured document containing electrical specifications, tables, operating ranges, application circuits, diagrams, pinouts, mechanical drawings, and packaging information. It is not a list of defects and does not describe known problems."
|
|
156
|
+
},
|
|
157
|
+
"USER_GUIDE": {
|
|
158
|
+
"name": "User Guide",
|
|
159
|
+
"description": "A practical, instructional document that explains how to use a hardware device, development board, evaluation kit, or module. A user guide focuses on setup, configuration, jumper settings, power requirements, interfaces, connectors, example usage, and safety notes. Unlike a datasheet, it avoids deep electrical specifications, register maps, or silicon details, and instead provides step-by-step procedures, diagrams, block overviews, and usage instructions intended for end users or developers."
|
|
160
|
+
},
|
|
161
|
+
"REFERENCE_MANUAL": {
|
|
162
|
+
"name": "Hardware Reference Manual",
|
|
163
|
+
"description": "A detailed, authoritative technical manual that defines the internal hardware architecture and low-level behavior of a device or system. It provides exhaustive functional descriptions of hardware blocks, registers, memory maps, address spaces, bit fields, timing relationships, and control logic. A hardware reference manual is used by firmware and driver developers for direct hardware interaction. It is not a datasheet, does not focus on electrical specifications or marketing summaries, and is not a step-by-step user guide."
|
|
164
|
+
},
|
|
165
|
+
"UNKNOWN": {
|
|
166
|
+
"name": "Unknown File",
|
|
167
|
+
"description": "File content does not match any known category such as BOM, PCB, netlist, schematic, datasheet, SPICE, or errata; used when insufficient or ambiguous information is present."
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Any other files subtypes
|
|
172
|
+
OtherFileTypeDescriptions: Dict[SupportedFileTypes, Dict[str, str]] = {
|
|
173
|
+
"BOM": {
|
|
174
|
+
"name": "Bill of Materials (BOM)",
|
|
175
|
+
"description": "Tabular component list containing part numbers, quantities, manufacturers, and descriptions; typically CSV/XLSX rows and columns; does not contain nets, schematic symbols, PCB layout, or SPICE commands."
|
|
176
|
+
},
|
|
177
|
+
"PADS_PCB_ASCII": {
|
|
178
|
+
"name": "PADS ASCII PCB",
|
|
179
|
+
"description": "ASCII export of a PADS PCB layout containing footprints, padstacks, copper geometry, vias, XY coordinates, and board definitions; does not contain schematic symbols, part tables, or SPICE simulation directives."
|
|
180
|
+
},
|
|
181
|
+
"KICAD_SPICE": {
|
|
182
|
+
"name": "KiCad SPICE Netlist",
|
|
183
|
+
"description": "SPICE circuit simulation file containing directives (.tran, .ac, .dc, .include), models, and device lines (R, C, L, V, I elements); does not contain PCB footprints, tables, or datasheet content."
|
|
184
|
+
},
|
|
185
|
+
"UNKNOWN": {
|
|
186
|
+
"name": "Unknown File",
|
|
187
|
+
"description": "File content does not match any known category such as BOM, PCB, netlist, schematic, datasheet, SPICE, or errata; used when insufficient or ambiguous information is present."
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MAX_GEMINI_PAGES = 1000
|
mfcli/crud/__init__.py
ADDED
|
File without changes
|
mfcli/crud/file.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from mfcli.models.file import File
|
|
2
|
+
from mfcli.models.file_metadata import FileMetadata
|
|
3
|
+
from mfcli.models.pipeline_run import PipelineRun
|
|
4
|
+
from mfcli.utils.logger import get_logger
|
|
5
|
+
from mfcli.utils.orm import Session
|
|
6
|
+
|
|
7
|
+
logger = get_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_file(db: Session, pipeline_run_id: int, metadata: FileMetadata) -> File:
|
|
11
|
+
logger.debug(f"Creating file: {metadata.name}")
|
|
12
|
+
existing_file: File | None = (
|
|
13
|
+
db.query(File)
|
|
14
|
+
.filter(File.md5 == metadata.md5)
|
|
15
|
+
.filter(File.pipeline_run_id == pipeline_run_id)
|
|
16
|
+
.one_or_none()
|
|
17
|
+
)
|
|
18
|
+
if existing_file:
|
|
19
|
+
raise ValueError(f"File {metadata.name} already exists and will not be processed")
|
|
20
|
+
|
|
21
|
+
pipeline_run: PipelineRun = (
|
|
22
|
+
db.query(PipelineRun)
|
|
23
|
+
.filter(PipelineRun.id == pipeline_run_id)
|
|
24
|
+
.one_or_none()
|
|
25
|
+
)
|
|
26
|
+
if not pipeline_run:
|
|
27
|
+
raise ValueError(f"Pipeline run ID does not exist: {pipeline_run_id}")
|
|
28
|
+
|
|
29
|
+
file = File(
|
|
30
|
+
name=metadata.name,
|
|
31
|
+
type=metadata.type_id,
|
|
32
|
+
mime_type=metadata.mime,
|
|
33
|
+
md5=metadata.md5,
|
|
34
|
+
path=str(metadata.path),
|
|
35
|
+
ext=metadata.ext,
|
|
36
|
+
is_datasheet=int(metadata.is_datasheet)
|
|
37
|
+
)
|
|
38
|
+
file.pipeline_run = pipeline_run
|
|
39
|
+
db.add(file)
|
|
40
|
+
db.flush()
|
|
41
|
+
logger.debug(f"File created: {file.id}")
|
|
42
|
+
return file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from mfcli.models.functional_blocks import FunctionalBlockMeta, FunctionalBlock, FunctionalBlockComponent
|
|
4
|
+
from mfcli.models.pipeline_run import PipelineRun
|
|
5
|
+
from mfcli.utils.orm import Session
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_functional_blocks(
|
|
9
|
+
db: Session,
|
|
10
|
+
pipeline_run: PipelineRun,
|
|
11
|
+
blocks: List[FunctionalBlockMeta]
|
|
12
|
+
) -> None:
|
|
13
|
+
db_blocks = []
|
|
14
|
+
for block in blocks:
|
|
15
|
+
db_block = FunctionalBlock(
|
|
16
|
+
name=block.name,
|
|
17
|
+
description=block.description,
|
|
18
|
+
pipeline_run=pipeline_run,
|
|
19
|
+
components=[]
|
|
20
|
+
)
|
|
21
|
+
for component_ref in (block.components or []):
|
|
22
|
+
db_block.components.append(
|
|
23
|
+
FunctionalBlockComponent(ref=component_ref)
|
|
24
|
+
)
|
|
25
|
+
db_blocks.append(db_block)
|
|
26
|
+
db.add_all(db_blocks)
|
mfcli/crud/netlist.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from mfcli.models.netlist import NetlistSchema, Netlist, NetlistComponent, NetlistPin
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def create_netlist(pipeline_run_id: int, netlist_schema: NetlistSchema) -> Netlist:
|
|
5
|
+
netlist = Netlist(pipeline_run_id=pipeline_run_id)
|
|
6
|
+
for component_schema in netlist_schema.components:
|
|
7
|
+
component = NetlistComponent(
|
|
8
|
+
ref_des=component_schema.ref_des,
|
|
9
|
+
part_number=component_schema.part_number
|
|
10
|
+
)
|
|
11
|
+
netlist.components.append(component)
|
|
12
|
+
for pin_schema in component_schema.pins:
|
|
13
|
+
pin = NetlistPin(
|
|
14
|
+
pin=pin_schema.pin,
|
|
15
|
+
net=pin_schema.net
|
|
16
|
+
)
|
|
17
|
+
component.pins.append(pin)
|
|
18
|
+
return netlist
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from mfcli.models.pipeline_run import PipelineRun
|
|
2
|
+
from mfcli.models.project import Project
|
|
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 create_pipeline_run(db: Session, project: Project) -> PipelineRun:
|
|
10
|
+
logger.debug("Creating pipeline run")
|
|
11
|
+
run = PipelineRun()
|
|
12
|
+
run.project = project
|
|
13
|
+
db.add(run)
|
|
14
|
+
db.flush()
|
|
15
|
+
logger.debug(f"Pipeline run created: {run.id}")
|
|
16
|
+
db.commit()
|
|
17
|
+
return run
|
mfcli/crud/project.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from mfcli.utils.directory_manager import app_dirs
|
|
10
|
+
from pydantic import ValidationError
|
|
11
|
+
|
|
12
|
+
from mfcli.models.project import Project, project_name_regex
|
|
13
|
+
from mfcli.models.project_metadata import ProjectConfig
|
|
14
|
+
from mfcli.utils.logger import get_logger
|
|
15
|
+
from mfcli.utils.migrations import run_migrations
|
|
16
|
+
from mfcli.utils.orm import Session
|
|
17
|
+
|
|
18
|
+
project_name_const = "Project name length must be between 3 and 45 and have any of these characters: [A-Za-z0-9_-]"
|
|
19
|
+
|
|
20
|
+
logger = get_logger()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_project(db: Session, repo_dir: str, name: str) -> Project:
|
|
24
|
+
logger.debug(f"Creating project with name: {name}")
|
|
25
|
+
name = name.strip()
|
|
26
|
+
if not re.match(project_name_regex, name):
|
|
27
|
+
raise ValueError(project_name_const)
|
|
28
|
+
existing_project = db.query(Project).filter(Project.name == name).one_or_none()
|
|
29
|
+
if existing_project:
|
|
30
|
+
raise ValueError("A project with this name already exists")
|
|
31
|
+
project = Project(
|
|
32
|
+
name=name,
|
|
33
|
+
repo_dir=str(Path(repo_dir)),
|
|
34
|
+
index_id=uuid4().hex
|
|
35
|
+
)
|
|
36
|
+
db.add(project)
|
|
37
|
+
db.flush()
|
|
38
|
+
logger.debug(f"Project created: {project}")
|
|
39
|
+
return project
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_project_by_name(db: Session, name: str) -> Project:
|
|
43
|
+
project: Project | None = (
|
|
44
|
+
db.query(Project)
|
|
45
|
+
.filter(Project.name == name)
|
|
46
|
+
.one_or_none()
|
|
47
|
+
)
|
|
48
|
+
if not project:
|
|
49
|
+
raise ValueError(f"A project by this name does not exist: {name}")
|
|
50
|
+
return project
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _list_projects(db: Session) -> List[Project]:
|
|
54
|
+
return db.query(Project).all()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def read_project_config_file() -> ProjectConfig:
|
|
58
|
+
file_path = app_dirs.config_file_path
|
|
59
|
+
logger.debug(f"Reading metadata file: {file_path}")
|
|
60
|
+
if not os.path.exists(file_path):
|
|
61
|
+
print('Could not find metadata file. Are you in the correct directory? Or if this is a new project, please initialize this repo with "mfcli init"')
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
if not os.access(file_path, os.R_OK):
|
|
64
|
+
print(f"Could not access repo file: {file_path}")
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
with open(file_path, "r") as f:
|
|
67
|
+
try:
|
|
68
|
+
return ProjectConfig(**json.loads(f.read()))
|
|
69
|
+
except ValidationError as e:
|
|
70
|
+
logger.error(f"Metadata file is corrupted: {file_path}")
|
|
71
|
+
raise e
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def init_project(project_name: str | None, repo_dir: Path | None = None):
|
|
75
|
+
try:
|
|
76
|
+
with Session() as db:
|
|
77
|
+
run_migrations()
|
|
78
|
+
if not repo_dir:
|
|
79
|
+
repo_dir = Path(os.getcwd())
|
|
80
|
+
logger.debug(f"Initializing project for repo dir: {repo_dir}")
|
|
81
|
+
file_path = app_dirs.config_file_path
|
|
82
|
+
if os.path.exists(file_path):
|
|
83
|
+
project_config = read_project_config_file()
|
|
84
|
+
get_project_by_name(db, project_config.name)
|
|
85
|
+
logger.debug(f"Project has already been initialized: {project_config.name}")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
repo_dir = str(repo_dir)
|
|
89
|
+
if not project_name:
|
|
90
|
+
project_name = input("Please choose a project name [A-Za-z0-9_-]: ")
|
|
91
|
+
create_project(db, repo_dir, project_name)
|
|
92
|
+
project_config = ProjectConfig(name=project_name)
|
|
93
|
+
logger.debug(f"Creating metadata file: {file_path}")
|
|
94
|
+
with open(file_path, "w") as f:
|
|
95
|
+
f.write(json.dumps(project_config.model_dump(), indent=2))
|
|
96
|
+
db.commit()
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.exception(e)
|
|
99
|
+
logger.error("Error initializing project")
|
|
File without changes
|
mfcli/digikey/digikey.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from requests import HTTPError
|
|
5
|
+
|
|
6
|
+
from mfcli.utils.config import get_config
|
|
7
|
+
from mfcli.utils.logger import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DigiKey:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self._config = get_config()
|
|
15
|
+
self._url = "https://api.digikey.com"
|
|
16
|
+
self._token: str | None = None
|
|
17
|
+
self._get_access_token()
|
|
18
|
+
self._session = requests.Session()
|
|
19
|
+
headers = {
|
|
20
|
+
"Authorization": f"Bearer {self._token}",
|
|
21
|
+
"X-DIGIKEY-Client-Id": self._config.digikey_client_id,
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
"Accept": "application/json"
|
|
24
|
+
}
|
|
25
|
+
self._session.headers = headers
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def _format_datasheet_url(url: str | None) -> str | None:
|
|
29
|
+
if url and url.startswith('//'):
|
|
30
|
+
return f"https:{url}"
|
|
31
|
+
return url
|
|
32
|
+
|
|
33
|
+
def _api_call(self, method: str, url: str, data: dict | None = None) -> dict:
|
|
34
|
+
max_retries = 5
|
|
35
|
+
timeout = 5
|
|
36
|
+
backoff_factor = 2
|
|
37
|
+
|
|
38
|
+
for attempt in range(1, max_retries + 1):
|
|
39
|
+
try:
|
|
40
|
+
resp = self._session.request(method=method, url=url, json=data, timeout=timeout)
|
|
41
|
+
|
|
42
|
+
if resp.status_code == 429:
|
|
43
|
+
if attempt == max_retries:
|
|
44
|
+
resp.raise_for_status()
|
|
45
|
+
|
|
46
|
+
retry_after = resp.headers.get("Retry-After")
|
|
47
|
+
delay = float(retry_after) if retry_after is not None else backoff_factor * (2 ** (attempt - 1))
|
|
48
|
+
time.sleep(delay)
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
resp.raise_for_status()
|
|
52
|
+
return resp.json()
|
|
53
|
+
|
|
54
|
+
except requests.exceptions.Timeout:
|
|
55
|
+
# Handle timeout — retry with exponential backoff
|
|
56
|
+
if attempt == max_retries:
|
|
57
|
+
raise # give up after last attempt
|
|
58
|
+
delay = backoff_factor * (2 ** (attempt - 1))
|
|
59
|
+
time.sleep(delay)
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
raise RuntimeError("Exhausted retries without success.")
|
|
63
|
+
|
|
64
|
+
def _get_access_token(self):
|
|
65
|
+
logger.debug("Fetching Digi-Key access token")
|
|
66
|
+
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
67
|
+
data = {
|
|
68
|
+
"client_id": self._config.digikey_client_id,
|
|
69
|
+
"client_secret": self._config.digikey_client_secret,
|
|
70
|
+
"grant_type": "client_credentials"
|
|
71
|
+
}
|
|
72
|
+
url = f"{self._url}/v1/oauth2/token"
|
|
73
|
+
resp = requests.post(url, headers=headers, data=data)
|
|
74
|
+
token_data = resp.json()
|
|
75
|
+
self._token = token_data["access_token"]
|
|
76
|
+
|
|
77
|
+
def _datasheet_from_part_number(self, part_number: str) -> str | None:
|
|
78
|
+
url = f"{self._url}/products/v4/search/{part_number}/productdetails"
|
|
79
|
+
resp = self._api_call('GET', url)
|
|
80
|
+
if not resp.get("Product"):
|
|
81
|
+
return None
|
|
82
|
+
return resp["Product"].get("DatasheetUrl")
|
|
83
|
+
|
|
84
|
+
def _keyword_search(self, part_number: str) -> str | None:
|
|
85
|
+
payload = {
|
|
86
|
+
"Keywords": part_number,
|
|
87
|
+
"RecordCount": 1
|
|
88
|
+
}
|
|
89
|
+
url = f"{self._url}/products/v4/search/keyword"
|
|
90
|
+
resp = self._api_call('POST', url, payload)
|
|
91
|
+
if not resp.get("ExactMatches"):
|
|
92
|
+
return None
|
|
93
|
+
return resp["ExactMatches"][0].get("DatasheetUrl")
|
|
94
|
+
|
|
95
|
+
def datasheet(self, part_number: str) -> str | None:
|
|
96
|
+
datasheet_url = None
|
|
97
|
+
try:
|
|
98
|
+
datasheet_url = self._datasheet_from_part_number(part_number)
|
|
99
|
+
except HTTPError as e:
|
|
100
|
+
# Handle 404 with keyword search API, otherwise raise HTTP error
|
|
101
|
+
if not e.response.status_code == 404:
|
|
102
|
+
raise e
|
|
103
|
+
if not datasheet_url:
|
|
104
|
+
datasheet_url = self._keyword_search(part_number)
|
|
105
|
+
return self._format_datasheet_url(datasheet_url)
|
mfcli/main.py
ADDED
mfcli/mcp/__init__.py
ADDED
|
File without changes
|
mfcli/mcp/server.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
import chromadb
|
|
4
|
+
from chromadb.utils import embedding_functions
|
|
5
|
+
|
|
6
|
+
from mfcli.mcp.mcp_instance import mcp
|
|
7
|
+
from mfcli.utils.config import get_config
|
|
8
|
+
from mfcli.utils.directory_manager import app_dirs
|
|
9
|
+
|
|
10
|
+
# Import tools to register them with mcp
|
|
11
|
+
import mfcli.mcp.tools.query_knowledgebase
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def chroma_db_connection_test():
|
|
15
|
+
try:
|
|
16
|
+
config = get_config()
|
|
17
|
+
test_chroma_client = chromadb.PersistentClient(path=app_dirs.chroma_db_dir)
|
|
18
|
+
test_openai_ef = embedding_functions.OpenAIEmbeddingFunction(
|
|
19
|
+
api_key=config.openai_api_key,
|
|
20
|
+
model_name=config.embedding_model
|
|
21
|
+
)
|
|
22
|
+
test_chroma_client.get_or_create_collection("engineering_docs", embedding_function=test_openai_ef)
|
|
23
|
+
print(f"✓ Connected to ChromaDB at: {app_dirs.chroma_db_dir}", file=sys.stderr, flush=True)
|
|
24
|
+
print(f"✓ Collection: engineering_docs", file=sys.stderr, flush=True)
|
|
25
|
+
except Exception as e:
|
|
26
|
+
print(f"✗ Failed to connect to ChromaDB: {e}", file=sys.stderr, flush=True)
|
|
27
|
+
raise
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main():
|
|
31
|
+
"""Entry point for mfcli-mcp command."""
|
|
32
|
+
chroma_db_connection_test()
|
|
33
|
+
mcp.run()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
main()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""State manager for MCP server to persist state between calls."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from mfcli.utils.directory_manager import app_dirs
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MCPStateManager:
|
|
10
|
+
"""Manages persistent state for the MCP server."""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
state_dir = Path(app_dirs.app_data_dir)
|
|
14
|
+
try:
|
|
15
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
except Exception:
|
|
17
|
+
# If we can't create the directory, we'll operate with in-memory state only.
|
|
18
|
+
pass
|
|
19
|
+
self.state_file = state_dir / "mcp_state.json"
|
|
20
|
+
self._state = self._load_state()
|
|
21
|
+
|
|
22
|
+
def _load_state(self) -> dict:
|
|
23
|
+
"""Load state from disk."""
|
|
24
|
+
if self.state_file.exists():
|
|
25
|
+
try:
|
|
26
|
+
with open(self.state_file, 'r') as f:
|
|
27
|
+
return json.load(f)
|
|
28
|
+
except Exception:
|
|
29
|
+
return {}
|
|
30
|
+
return {}
|
|
31
|
+
|
|
32
|
+
def _save_state(self):
|
|
33
|
+
"""Save state to disk."""
|
|
34
|
+
try:
|
|
35
|
+
with open(self.state_file, 'w') as f:
|
|
36
|
+
json.dump(self._state, f, indent=2)
|
|
37
|
+
except Exception:
|
|
38
|
+
pass # Silently fail if we can't save state
|
|
39
|
+
|
|
40
|
+
def get_last_project_name(self) -> Optional[str]:
|
|
41
|
+
"""Get the last used project name."""
|
|
42
|
+
return self._state.get('last_project_name')
|
|
43
|
+
|
|
44
|
+
def set_last_project_name(self, project_name: str):
|
|
45
|
+
"""Set the last used project name."""
|
|
46
|
+
self._state['last_project_name'] = project_name
|
|
47
|
+
self._save_state()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Singleton instance
|
|
51
|
+
state_manager = MCPStateManager()
|
|
File without changes
|