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,94 @@
|
|
|
1
|
+
from typing import List, Dict, Tuple
|
|
2
|
+
from difflib import SequenceMatcher
|
|
3
|
+
|
|
4
|
+
from mfcli.models.functional_blocks import FunctionalBlockMeta
|
|
5
|
+
from mfcli.models.netlist import NetlistComponent, Netlist
|
|
6
|
+
from mfcli.utils.logger import get_logger
|
|
7
|
+
from mfcli.utils.orm import Session
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_functional_blocks_against_netlist(
|
|
13
|
+
db: Session,
|
|
14
|
+
pipeline_run_id: int,
|
|
15
|
+
functional_blocks: List[FunctionalBlockMeta]
|
|
16
|
+
) -> Dict:
|
|
17
|
+
"""
|
|
18
|
+
Validate functional block components against netlist.
|
|
19
|
+
Returns corrected blocks and validation report.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Get all valid component ref_des from netlist
|
|
23
|
+
valid_components = (
|
|
24
|
+
db.query(NetlistComponent.ref_des)
|
|
25
|
+
.join(NetlistComponent.netlist)
|
|
26
|
+
.filter(Netlist.pipeline_run_id == pipeline_run_id)
|
|
27
|
+
.all()
|
|
28
|
+
)
|
|
29
|
+
valid_refs = {comp.ref_des for comp in valid_components}
|
|
30
|
+
|
|
31
|
+
report = {
|
|
32
|
+
"total_components": 0,
|
|
33
|
+
"valid_components": 0,
|
|
34
|
+
"invalid_components": [],
|
|
35
|
+
"corrected_components": [],
|
|
36
|
+
"blocks_validated": 0
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for block in functional_blocks:
|
|
40
|
+
corrected_components = []
|
|
41
|
+
|
|
42
|
+
for component_ref in block.components:
|
|
43
|
+
report["total_components"] += 1
|
|
44
|
+
|
|
45
|
+
if component_ref in valid_refs:
|
|
46
|
+
# Valid component
|
|
47
|
+
report["valid_components"] += 1
|
|
48
|
+
corrected_components.append(component_ref)
|
|
49
|
+
else:
|
|
50
|
+
# Invalid - try fuzzy matching
|
|
51
|
+
best_match = find_best_match(component_ref, valid_refs)
|
|
52
|
+
|
|
53
|
+
if best_match and similarity_ratio(component_ref, best_match) > 0.8:
|
|
54
|
+
# High confidence match - autocorrect
|
|
55
|
+
logger.warning(f"Auto-correcting '{component_ref}' -> '{best_match}'")
|
|
56
|
+
corrected_components.append(best_match)
|
|
57
|
+
report["corrected_components"].append({
|
|
58
|
+
"original": component_ref,
|
|
59
|
+
"corrected": best_match,
|
|
60
|
+
"block": block.name
|
|
61
|
+
})
|
|
62
|
+
else:
|
|
63
|
+
# No good match - log and skip
|
|
64
|
+
logger.error(f"Invalid component '{component_ref}' in block '{block.name}'")
|
|
65
|
+
report["invalid_components"].append({
|
|
66
|
+
"ref": component_ref,
|
|
67
|
+
"block": block.name,
|
|
68
|
+
"suggestion": best_match if best_match else None
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
block.components = corrected_components
|
|
72
|
+
|
|
73
|
+
report["blocks_validated"] += 1
|
|
74
|
+
|
|
75
|
+
return report
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def find_best_match(ref: str, valid_refs: set) -> str:
|
|
79
|
+
"""Find nearest matching reference designator."""
|
|
80
|
+
best_match = None
|
|
81
|
+
best_ratio = 0
|
|
82
|
+
|
|
83
|
+
for valid_ref in valid_refs:
|
|
84
|
+
ratio = SequenceMatcher(None, ref.upper(), valid_ref.upper()).ratio()
|
|
85
|
+
if ratio > best_ratio:
|
|
86
|
+
best_ratio = ratio
|
|
87
|
+
best_match = valid_ref
|
|
88
|
+
|
|
89
|
+
return best_match if best_ratio > 0.6 else None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def similarity_ratio(str1: str, str2: str) -> float:
|
|
93
|
+
"""Calculate similarity between two strings."""
|
|
94
|
+
return SequenceMatcher(None, str1.upper(), str2.upper()).ratio()
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, Any, List, Type
|
|
5
|
+
|
|
6
|
+
from google.genai.types import File as GeminiFile
|
|
7
|
+
|
|
8
|
+
from mfcli.client.gemini import GeminiFileEntity
|
|
9
|
+
from mfcli.constants.file_types import FileSubtypes
|
|
10
|
+
from mfcli.models.bom import BOMSchema
|
|
11
|
+
from mfcli.models.file import File
|
|
12
|
+
from mfcli.models.file_docket import FileDocketEntry
|
|
13
|
+
from mfcli.pipeline.analysis.generators.bom.bom import BOMGenerator
|
|
14
|
+
from mfcli.pipeline.analysis.generators.debug_setup.debug_setup import DSCheatSheetGenerator
|
|
15
|
+
from mfcli.pipeline.analysis.generators.functional_blocks.functional_blocks import (
|
|
16
|
+
extract_functional_blocks_from_file,
|
|
17
|
+
FBCheatSheetGenerator
|
|
18
|
+
)
|
|
19
|
+
from mfcli.pipeline.analysis.generators.generator_base import GeneratorBase
|
|
20
|
+
from mfcli.pipeline.analysis.generators.mcu.mcu import MCUCheatSheetGenerator
|
|
21
|
+
from mfcli.pipeline.analysis.generators.mcu_errata.mcu_errata import ErrataCheatSheetGenerator
|
|
22
|
+
from mfcli.pipeline.analysis.generators.summary.summary import SummaryCheatSheetGenerator
|
|
23
|
+
from mfcli.pipeline.run_context import PipelineRunContext
|
|
24
|
+
from mfcli.utils.directory_manager import app_dirs
|
|
25
|
+
from mfcli.utils.logger import get_logger
|
|
26
|
+
|
|
27
|
+
logger = get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
FileSubtypeGeneratorMap: Dict[FileSubtypes, List[Type[GeneratorBase]]] = {
|
|
30
|
+
FileSubtypes.ERRATA: [ErrataCheatSheetGenerator],
|
|
31
|
+
FileSubtypes.MCU_DATASHEET: [MCUCheatSheetGenerator],
|
|
32
|
+
FileSubtypes.SCHEMATIC: [DSCheatSheetGenerator],
|
|
33
|
+
FileSubtypes.USER_GUIDE: [SummaryCheatSheetGenerator],
|
|
34
|
+
FileSubtypes.REFERENCE_MANUAL: [SummaryCheatSheetGenerator]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Mapping of generator classes to friendly output names
|
|
38
|
+
GeneratorNameMap: Dict[Any, str] = {
|
|
39
|
+
ErrataCheatSheetGenerator: "mcu_errata",
|
|
40
|
+
MCUCheatSheetGenerator: "mcu",
|
|
41
|
+
DSCheatSheetGenerator: "debug_setup",
|
|
42
|
+
FBCheatSheetGenerator: "functional_blocks",
|
|
43
|
+
SummaryCheatSheetGenerator: "summary"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Generator:
|
|
48
|
+
def __init__(self, context: PipelineRunContext, processed_file_types: set[str] = None):
|
|
49
|
+
self._context = context
|
|
50
|
+
self._processed_file_types = processed_file_types or set()
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def _create_cheat_sheet_json_file(file_path: str, cheat_sheet_data: Dict) -> str:
|
|
54
|
+
# Ensure parent directory exists
|
|
55
|
+
Path(file_path).parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
with open(file_path, 'w', encoding='utf-8', errors='replace') as jsonfile:
|
|
57
|
+
data = json.dumps(cheat_sheet_data, indent=2, ensure_ascii=False)
|
|
58
|
+
jsonfile.write(data)
|
|
59
|
+
return data
|
|
60
|
+
|
|
61
|
+
def _add_to_file_docket(self, file_name: str, file_path: str, vectorize: bool, sub_type: str):
|
|
62
|
+
entry = FileDocketEntry(
|
|
63
|
+
name=file_name,
|
|
64
|
+
path=str(file_path),
|
|
65
|
+
vectorize=vectorize,
|
|
66
|
+
sub_type=sub_type
|
|
67
|
+
)
|
|
68
|
+
self._context.docket.add(entry)
|
|
69
|
+
|
|
70
|
+
def _file_subtype_query(self, subtype: FileSubtypes):
|
|
71
|
+
return (
|
|
72
|
+
self._context.db.query(File)
|
|
73
|
+
.filter(File.pipeline_run_id == self._context.run.id)
|
|
74
|
+
.filter(File.sub_type == subtype)
|
|
75
|
+
.filter(File.is_datasheet == 0)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def _query_first_file_subtype(self, subtype: FileSubtypes) -> File | None:
|
|
79
|
+
return self._file_subtype_query(subtype).first()
|
|
80
|
+
|
|
81
|
+
def _query_all_file_subtype(self, subtype: FileSubtypes) -> List[File]:
|
|
82
|
+
return self._file_subtype_query(subtype).all()
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def _create_bom_csv(file_path: str, bom_entries: List[Dict]):
|
|
86
|
+
# Ensure parent directory exists
|
|
87
|
+
Path(file_path).parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
columns = list(BOMSchema.model_fields.keys())
|
|
89
|
+
with open(file_path, "w", newline="", encoding="utf-8", errors="ignore") as f:
|
|
90
|
+
writer = csv.DictWriter(f, fieldnames=columns)
|
|
91
|
+
writer.writeheader()
|
|
92
|
+
writer.writerows(bom_entries)
|
|
93
|
+
|
|
94
|
+
def _vectorize_bom_csv(self, file_path: str):
|
|
95
|
+
with open(file_path, "r") as f:
|
|
96
|
+
self._context.vectorizer.vectorize_text_content(f.read(), "bom.csv", "BOM")
|
|
97
|
+
|
|
98
|
+
def _get_gemini_file(self, file: File) -> GeminiFile | None:
|
|
99
|
+
if not self._context.gemini_file_cache.get(file.gemini_file_id):
|
|
100
|
+
logger.warn(f"No Gemini file found for file: {file.id}")
|
|
101
|
+
return
|
|
102
|
+
return self._context.gemini_file_cache[file.gemini_file_id]
|
|
103
|
+
|
|
104
|
+
async def _generate_bom(self):
|
|
105
|
+
bom_file = self._query_first_file_subtype(FileSubtypes.BOM)
|
|
106
|
+
if bom_file:
|
|
107
|
+
return
|
|
108
|
+
logger.info("Generation BOM")
|
|
109
|
+
generator = BOMGenerator(self._context.gemini)
|
|
110
|
+
schematic_file = self._query_first_file_subtype(FileSubtypes.SCHEMATIC)
|
|
111
|
+
if not schematic_file:
|
|
112
|
+
return
|
|
113
|
+
gemini_file = self._get_gemini_file(schematic_file)
|
|
114
|
+
if not gemini_file:
|
|
115
|
+
return
|
|
116
|
+
bom_entries = await generator.generate(gemini_file)
|
|
117
|
+
file_path = app_dirs.generated_files_dir / "bom.csv"
|
|
118
|
+
self._create_bom_csv(file_path, bom_entries)
|
|
119
|
+
if self._context.config.vectorize_hw_files:
|
|
120
|
+
self._vectorize_bom_csv(file_path)
|
|
121
|
+
self._add_to_file_docket("bom.csv", file_path, self._context.config.vectorize_hw_files, "BOM")
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _get_generator_name(generator_type: Any) -> str:
|
|
125
|
+
"""Get the friendly name for a generator class."""
|
|
126
|
+
return GeneratorNameMap.get(generator_type, generator_type.__name__.lower())
|
|
127
|
+
|
|
128
|
+
def _get_cs_vectorization_setting(self, cs_type: Any) -> bool:
|
|
129
|
+
vectorize_config = self._context.config.vectorize_cheat_sheets_config
|
|
130
|
+
should_vectorize = False
|
|
131
|
+
if cs_type == ErrataCheatSheetGenerator:
|
|
132
|
+
should_vectorize = vectorize_config.vectorize_errata
|
|
133
|
+
elif cs_type == MCUCheatSheetGenerator:
|
|
134
|
+
should_vectorize = vectorize_config.vectorize_mcu
|
|
135
|
+
elif cs_type == DSCheatSheetGenerator:
|
|
136
|
+
should_vectorize = vectorize_config.vectorize_debug_setup
|
|
137
|
+
elif cs_type == FBCheatSheetGenerator:
|
|
138
|
+
should_vectorize = vectorize_config.vectorize_functional_blocks
|
|
139
|
+
return should_vectorize
|
|
140
|
+
|
|
141
|
+
def _get_gemini_files_from_pdf(self, file: File) -> List[GeminiFile]:
|
|
142
|
+
if not file.pdf_parts:
|
|
143
|
+
return [self._get_gemini_file(file)]
|
|
144
|
+
return [self._get_gemini_file(part) for part in file.pdf_parts]
|
|
145
|
+
|
|
146
|
+
async def _generate_functional_blocks(self):
|
|
147
|
+
# TODO: HANDLE CASE OF MULTIPLE SCHEMATIC/USER GUIDES/NETLISTS
|
|
148
|
+
schematic = self._query_first_file_subtype(FileSubtypes.SCHEMATIC)
|
|
149
|
+
schematic_files = self._get_gemini_files_from_pdf(schematic) if schematic else None
|
|
150
|
+
user_guide = self._query_first_file_subtype(FileSubtypes.USER_GUIDE)
|
|
151
|
+
user_guide_files = self._get_gemini_files_from_pdf(user_guide) if user_guide else None
|
|
152
|
+
|
|
153
|
+
# TODO: QUERY DIFFERENT NETLISTS
|
|
154
|
+
netlist = self._query_first_file_subtype(FileSubtypes.PROTEL_ALTIUM)
|
|
155
|
+
|
|
156
|
+
# Upload netlist to Gemini because it's not a PDF, hasn't been uploaded yet
|
|
157
|
+
netlist_file = None
|
|
158
|
+
if netlist:
|
|
159
|
+
netlist_gemini_file_meta = GeminiFileEntity(
|
|
160
|
+
path=Path(netlist.path),
|
|
161
|
+
mime_type="text/plain"
|
|
162
|
+
)
|
|
163
|
+
netlist_file = await self._context.gemini.upload(netlist_gemini_file_meta)
|
|
164
|
+
|
|
165
|
+
if schematic_files or user_guide_files or netlist_file:
|
|
166
|
+
logger.info("Generating functional blocks cheat sheet")
|
|
167
|
+
|
|
168
|
+
# Generate functional blocks from uploaded files
|
|
169
|
+
block_data = await extract_functional_blocks_from_file(
|
|
170
|
+
db=self._context.db,
|
|
171
|
+
pipeline_run=self._context.run,
|
|
172
|
+
gemini=self._context.gemini,
|
|
173
|
+
schematic_files=schematic_files,
|
|
174
|
+
user_guide_files=user_guide_files,
|
|
175
|
+
netlist_file=netlist_file
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Create cheat sheet JSON file
|
|
179
|
+
generator_name = self._get_generator_name(FBCheatSheetGenerator)
|
|
180
|
+
file_name = f"{schematic.name}_{generator_name}_cheat_sheet.json"
|
|
181
|
+
file_path = app_dirs.cheat_sheets_dir / file_name
|
|
182
|
+
text = self._create_cheat_sheet_json_file(file_path, block_data)
|
|
183
|
+
|
|
184
|
+
# Vectorize file and add to file docket
|
|
185
|
+
self._vectorize_and_add_to_docket(FBCheatSheetGenerator, file_path, file_name, text)
|
|
186
|
+
|
|
187
|
+
# Commit functional blocks to DB
|
|
188
|
+
self._context.db.commit()
|
|
189
|
+
|
|
190
|
+
def _vectorize_and_add_to_docket(self, cs_type: Any, file_path: str, file_name: str, text: str):
|
|
191
|
+
logger.debug(f"Vectorizing and adding cheat sheet to docket: {file_name}")
|
|
192
|
+
should_vectorize = self._get_cs_vectorization_setting(cs_type)
|
|
193
|
+
logger.debug(f"Should vectorize: {should_vectorize}")
|
|
194
|
+
if should_vectorize:
|
|
195
|
+
logger.debug(f"Vectorizing cheat sheet: {file_name}")
|
|
196
|
+
self._context.vectorizer.vectorize_text_content(text, file_name, "CHEAT_SHEET")
|
|
197
|
+
logger.debug(f"Adding cheat sheet to docket: {file_name}")
|
|
198
|
+
self._add_to_file_docket(file_name, file_path, should_vectorize, "CHEAT_SHEET")
|
|
199
|
+
|
|
200
|
+
async def _generate_for_subtype(self, subtype: FileSubtypes, generators: List[Type[GeneratorBase]]):
|
|
201
|
+
files = self._query_all_file_subtype(subtype)
|
|
202
|
+
for file in files:
|
|
203
|
+
gemini_files = self._get_gemini_files_from_pdf(file)
|
|
204
|
+
if not gemini_files:
|
|
205
|
+
continue
|
|
206
|
+
for generator in generators:
|
|
207
|
+
gen = generator(self._context, file, gemini_files)
|
|
208
|
+
generator_name = self._get_generator_name(generator)
|
|
209
|
+
try:
|
|
210
|
+
logger.info(f"Generating {generator_name} cheat sheet")
|
|
211
|
+
cheat_sheet_data = await gen.generate()
|
|
212
|
+
if not cheat_sheet_data:
|
|
213
|
+
continue
|
|
214
|
+
file_name = f"{file.name}_{generator_name}_cheat_sheet.json"
|
|
215
|
+
file_path = app_dirs.cheat_sheets_dir / file_name
|
|
216
|
+
text = self._create_cheat_sheet_json_file(file_path, cheat_sheet_data)
|
|
217
|
+
self._vectorize_and_add_to_docket(generator, file_path, file_name, text)
|
|
218
|
+
except Exception as e:
|
|
219
|
+
logger.exception(e)
|
|
220
|
+
logger.error(f"Error generating cheat sheet from {generator_name}")
|
|
221
|
+
|
|
222
|
+
async def generate_cheat_sheets(self):
|
|
223
|
+
# Only generate BOM if schematic was processed
|
|
224
|
+
if "SCHEMATIC" in self._processed_file_types:
|
|
225
|
+
logger.info("Generating BOM (schematic was processed)")
|
|
226
|
+
try:
|
|
227
|
+
await self._generate_bom()
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.exception(e)
|
|
230
|
+
logger.error(f"Error generating BOM: {self._context.run.id}")
|
|
231
|
+
else:
|
|
232
|
+
logger.info("Skipping BOM generation (schematic not processed)")
|
|
233
|
+
|
|
234
|
+
# Only generate functional blocks if schematic, user guide, or netlist was processed
|
|
235
|
+
netlist_types = {"PROTEL_ALTIUM", "KICAD_LEGACY_NET", "KICAD_SPICE", "PADS", "EDIF"}
|
|
236
|
+
should_generate_fb = (
|
|
237
|
+
"SCHEMATIC" in self._processed_file_types or
|
|
238
|
+
"USER_GUIDE" in self._processed_file_types or
|
|
239
|
+
any(nt in self._processed_file_types for nt in netlist_types)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if should_generate_fb:
|
|
243
|
+
logger.info("Generating functional blocks (schematic/user guide/netlist was processed)")
|
|
244
|
+
try:
|
|
245
|
+
await self._generate_functional_blocks()
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.exception(e)
|
|
248
|
+
logger.error(f"Error generating functional blocks: {self._context.run.id}")
|
|
249
|
+
else:
|
|
250
|
+
logger.info("Skipping functional blocks generation (no relevant files processed)")
|
|
251
|
+
|
|
252
|
+
# Generate cheat sheets for processed file types only
|
|
253
|
+
for subtype, generators in FileSubtypeGeneratorMap.items():
|
|
254
|
+
if subtype.name in self._processed_file_types:
|
|
255
|
+
logger.info(f"Generating cheat sheets for {subtype.name} (file was processed)")
|
|
256
|
+
await self._generate_for_subtype(subtype, generators)
|
|
257
|
+
else:
|
|
258
|
+
logger.info(f"Skipping cheat sheet generation for {subtype.name} (file not processed)")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from typing import List, Dict
|
|
3
|
+
|
|
4
|
+
from google.genai.types import File as GeminiFile
|
|
5
|
+
|
|
6
|
+
from mfcli.models.file import File
|
|
7
|
+
from mfcli.pipeline.run_context import PipelineRunContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GeneratorBase:
|
|
11
|
+
def __init__(self, context: PipelineRunContext, db_file: File, uploads: List[GeminiFile]):
|
|
12
|
+
self._context = context
|
|
13
|
+
self._file = db_file
|
|
14
|
+
self._uploads: List[GeminiFile] = uploads
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
async def generate(self) -> Dict:
|
|
18
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
mcu_instructions_base = (
|
|
2
|
+
"""
|
|
3
|
+
You are the MCU Cheat Sheet Generator agent. Your role is to analyze microcontroller
|
|
4
|
+
datasheets and user manuals to extract critical information needed for creating initial
|
|
5
|
+
firmware boot code, linker scripts, and basic "hello world" applications.
|
|
6
|
+
"""
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
mcu_data_instruction = (
|
|
10
|
+
"""
|
|
11
|
+
Extract the following information systematically:
|
|
12
|
+
MCU IDENTIFICATION:
|
|
13
|
+
- mcu_name: Full part number (e.g., "STM32F407VGT6", "ATMEGA328P", "MSPM0L1306")
|
|
14
|
+
- mcu_family: Family or series name
|
|
15
|
+
- architecture: CPU core architecture (ARM Cortex-M4, AVR, RISC-V, etc.)
|
|
16
|
+
- core_frequency_max: Maximum operating frequency
|
|
17
|
+
"""
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
mcu_memory_map_instructions = (
|
|
21
|
+
"""
|
|
22
|
+
Extract the following information systematically:
|
|
23
|
+
MEMORY MAP (CRITICAL for linker script!):
|
|
24
|
+
For each memory region, extract:
|
|
25
|
+
- name: FLASH, RAM, ROM, SRAM, etc.
|
|
26
|
+
- start_address: Starting address in hex (e.g., "0x08000000")
|
|
27
|
+
- size: Size with unit (e.g., "128KB", "0x20000")
|
|
28
|
+
- type: flash, ram, rom, peripheral, reserved
|
|
29
|
+
- description: Any important notes about this region
|
|
30
|
+
|
|
31
|
+
Look in: Memory organization, memory map tables, address space layout sections
|
|
32
|
+
"""
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
mcu_clock_config_instructions = (
|
|
36
|
+
"""
|
|
37
|
+
Extract the following information systematically:
|
|
38
|
+
CLOCK CONFIGURATION:
|
|
39
|
+
- clock_source: Available clock sources (HSI, HSE, PLL, Internal RC, etc.)
|
|
40
|
+
- frequency_range: Typical or maximum frequencies
|
|
41
|
+
- configuration_steps: How to set up the system clock
|
|
42
|
+
- registers: Key clock control registers (RCC, CCR, etc.)
|
|
43
|
+
- notes: Important clock-related information
|
|
44
|
+
|
|
45
|
+
Look in: Clock tree diagrams, RCC chapter, system clock sections
|
|
46
|
+
"""
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
mcu_interrupt_vectors_instructions = (
|
|
50
|
+
"""
|
|
51
|
+
Extract the following information systematically:
|
|
52
|
+
INTERRUPT VECTORS:
|
|
53
|
+
Extract the key interrupt vectors (at least the essential ones):
|
|
54
|
+
- vector_number: Position in vector table
|
|
55
|
+
- name: Interrupt name (Reset, NMI, HardFault, SysTick, etc.)
|
|
56
|
+
- address: Vector table address if specified
|
|
57
|
+
- priority: Default or configurable priority
|
|
58
|
+
- description: What triggers this interrupt
|
|
59
|
+
|
|
60
|
+
Look in: Interrupt vector table, exception handling, NVIC sections
|
|
61
|
+
|
|
62
|
+
Priority: Focus on:
|
|
63
|
+
- Reset vector (most important!)
|
|
64
|
+
- NMI, HardFault, SysTick (ARM Cortex)
|
|
65
|
+
- Common peripheral interrupts
|
|
66
|
+
"""
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
mcu_startup_req_instructions = (
|
|
70
|
+
"""
|
|
71
|
+
Extract the following information systematically:
|
|
72
|
+
STARTUP REQUIREMENTS:
|
|
73
|
+
- boot_mode: How boot mode is selected
|
|
74
|
+
- startup_code_location: Where startup code must be placed
|
|
75
|
+
- vector_table_offset: VTOR register info (ARM Cortex)
|
|
76
|
+
- stack_pointer_init: How to set up stack pointer
|
|
77
|
+
- minimum_init_sequence: Bare minimum steps to boot successfully
|
|
78
|
+
- watchdog_handling: Watchdog timer at startup
|
|
79
|
+
|
|
80
|
+
Look in: Boot sequence, startup procedure, reset behavior sections
|
|
81
|
+
"""
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
mcu_peripherals_instructions = (
|
|
85
|
+
"""
|
|
86
|
+
Extract the following information systematically:
|
|
87
|
+
ESSENTIAL PERIPHERALS:
|
|
88
|
+
For GPIO, UART, or other basic peripherals:
|
|
89
|
+
- name: Peripheral name
|
|
90
|
+
- base_address: Base address in memory map
|
|
91
|
+
- clock_enable: How to enable peripheral clock
|
|
92
|
+
- initialization_steps: Basic init procedure
|
|
93
|
+
- key_registers: Important registers
|
|
94
|
+
|
|
95
|
+
Focus on GPIO and UART for a basic hello world app
|
|
96
|
+
"""
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
mcu_linker_script_instructions = (
|
|
100
|
+
"""
|
|
101
|
+
Extract the following information systematically:
|
|
102
|
+
LINKER SCRIPT NOTES:
|
|
103
|
+
Provide guidance for creating a linker script:
|
|
104
|
+
- Memory region definitions (FLASH, RAM, etc.)
|
|
105
|
+
- Entry point specification
|
|
106
|
+
- Section placement (.text, .data, .bss)
|
|
107
|
+
- Stack and heap allocation
|
|
108
|
+
- Vector table placement
|
|
109
|
+
- Any special considerations
|
|
110
|
+
"""
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
mcu_hello_world_instructions = (
|
|
114
|
+
"""
|
|
115
|
+
Extract the following information systematically:
|
|
116
|
+
HELLO WORLD STEPS:
|
|
117
|
+
List the steps to create a minimal working program:
|
|
118
|
+
1. Set up linker script
|
|
119
|
+
2. Create startup code
|
|
120
|
+
3. Initialize system clock
|
|
121
|
+
4. Configure GPIO/UART
|
|
122
|
+
5. Write main() function
|
|
123
|
+
6. Build and flash
|
|
124
|
+
|
|
125
|
+
Be specific about what needs to be done at each step
|
|
126
|
+
"""
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
mcu_debugging_interface_instructions = (
|
|
130
|
+
"""
|
|
131
|
+
Extract the following information systematically:
|
|
132
|
+
DEBUGGING/PROGRAMMING INTERFACE:
|
|
133
|
+
- debugging_interface: JTAG, SWD, ICSP, etc.
|
|
134
|
+
- programming_interface: How to program the device
|
|
135
|
+
"""
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
mcu_programming_interface_instructions = (
|
|
139
|
+
"""
|
|
140
|
+
Extract the following information systematically:
|
|
141
|
+
DEBUGGING/PROGRAMMING INTERFACE:
|
|
142
|
+
- debugging_interface: JTAG, SWD, ICSP, etc.
|
|
143
|
+
- programming_interface: How to program the device
|
|
144
|
+
"""
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
mcu_datasheet_references = (
|
|
148
|
+
"""
|
|
149
|
+
Extract the following information systematically:
|
|
150
|
+
DATASHEET REFERENCES:
|
|
151
|
+
List specific sections/pages that were most useful in this document:
|
|
152
|
+
- "Memory Map - Section 2.2, Page 45"
|
|
153
|
+
- "Clock Tree - Figure 15, Page 78"
|
|
154
|
+
etc.
|
|
155
|
+
"""
|
|
156
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Dict, Type, List
|
|
3
|
+
|
|
4
|
+
from google.genai.types import File as GeminiFile
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from mfcli.models.file import File
|
|
8
|
+
from mfcli.models.mcu import (
|
|
9
|
+
MCUData,
|
|
10
|
+
MemoryMap,
|
|
11
|
+
ClockConfig,
|
|
12
|
+
InterruptVectors,
|
|
13
|
+
StartupRequirementsModel,
|
|
14
|
+
EssentialPeripherals,
|
|
15
|
+
MCULinkerScriptInfo,
|
|
16
|
+
MCUHelloWorldSteps,
|
|
17
|
+
MCUDebuggingInterface,
|
|
18
|
+
MCUProgrammingInterface,
|
|
19
|
+
MCUDatasheetReferences
|
|
20
|
+
)
|
|
21
|
+
from mfcli.pipeline.analysis.generators.generator_base import GeneratorBase
|
|
22
|
+
from mfcli.pipeline.analysis.generators.mcu.instructions import (
|
|
23
|
+
mcu_data_instruction,
|
|
24
|
+
mcu_instructions_base,
|
|
25
|
+
mcu_memory_map_instructions,
|
|
26
|
+
mcu_clock_config_instructions,
|
|
27
|
+
mcu_interrupt_vectors_instructions,
|
|
28
|
+
mcu_startup_req_instructions,
|
|
29
|
+
mcu_peripherals_instructions,
|
|
30
|
+
mcu_linker_script_instructions,
|
|
31
|
+
mcu_hello_world_instructions,
|
|
32
|
+
mcu_debugging_interface_instructions,
|
|
33
|
+
mcu_programming_interface_instructions,
|
|
34
|
+
mcu_datasheet_references
|
|
35
|
+
)
|
|
36
|
+
from mfcli.pipeline.run_context import PipelineRunContext
|
|
37
|
+
from mfcli.utils.logger import get_logger
|
|
38
|
+
|
|
39
|
+
logger = get_logger(__name__)
|
|
40
|
+
|
|
41
|
+
MCUModelInstructionsMap: Dict[Type[BaseModel], str] = {
|
|
42
|
+
MCUData: mcu_data_instruction,
|
|
43
|
+
MemoryMap: mcu_memory_map_instructions,
|
|
44
|
+
ClockConfig: mcu_clock_config_instructions,
|
|
45
|
+
InterruptVectors: mcu_interrupt_vectors_instructions,
|
|
46
|
+
StartupRequirementsModel: mcu_startup_req_instructions,
|
|
47
|
+
EssentialPeripherals: mcu_peripherals_instructions,
|
|
48
|
+
MCULinkerScriptInfo: mcu_linker_script_instructions,
|
|
49
|
+
MCUHelloWorldSteps: mcu_hello_world_instructions,
|
|
50
|
+
MCUDebuggingInterface: mcu_debugging_interface_instructions,
|
|
51
|
+
MCUProgrammingInterface: mcu_programming_interface_instructions,
|
|
52
|
+
MCUDatasheetReferences: mcu_datasheet_references
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class MCUCheatSheetGenerator(GeneratorBase):
|
|
57
|
+
def __init__(self, context: PipelineRunContext, db_file: File, uploads: List[GeminiFile]):
|
|
58
|
+
super().__init__(context, db_file, uploads)
|
|
59
|
+
|
|
60
|
+
async def _extract_mcu_data(self, model: Type[BaseModel], instructions: str) -> BaseModel:
|
|
61
|
+
return await self._context.gemini.generate(
|
|
62
|
+
prompt=instructions,
|
|
63
|
+
instructions=mcu_instructions_base,
|
|
64
|
+
response_model=model,
|
|
65
|
+
files=self._uploads
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def _parse_mcu_generated_data(results: list[BaseModel]) -> Dict:
|
|
70
|
+
response_json = {}
|
|
71
|
+
for result in results:
|
|
72
|
+
response_json.update(result.model_dump())
|
|
73
|
+
return {
|
|
74
|
+
"mcu_cheat_sheet": response_json
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async def generate(self) -> Dict:
|
|
78
|
+
logger.debug(f"Generating MCU data cheat sheet")
|
|
79
|
+
tasks = []
|
|
80
|
+
for model, instructions in MCUModelInstructionsMap.items():
|
|
81
|
+
tasks.append(self._extract_mcu_data(model, instructions))
|
|
82
|
+
results: list[BaseModel] = await asyncio.gather(*tasks)
|
|
83
|
+
logger.debug(f"MCU data cheat sheet generated")
|
|
84
|
+
return self._parse_mcu_generated_data(results)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCU Errata Cheat Sheet Generator for firmware-relevant silicon errata."""
|