mfcli 0.2.1__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 +200 -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 +144 -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 +470 -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/cline_rules.py +256 -0
- mfcli/utils/config.py +33 -0
- mfcli/utils/configurator.py +324 -0
- mfcli/utils/data_cleaner.py +114 -0
- mfcli/utils/datasheet_vectorizer.py +283 -0
- mfcli/utils/directory_manager.py +116 -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 +394 -0
- mfcli/utils/migrations.py +18 -0
- mfcli/utils/orm.py +43 -0
- mfcli/utils/pdf_splitter.py +63 -0
- mfcli/utils/pre_uninstall.py +167 -0
- mfcli/utils/query_service.py +22 -0
- mfcli/utils/system_check.py +306 -0
- mfcli/utils/tools.py +98 -0
- mfcli/utils/vectorizer.py +28 -0
- mfcli-0.2.1.dist-info/METADATA +956 -0
- mfcli-0.2.1.dist-info/RECORD +138 -0
- mfcli-0.2.1.dist-info/WHEEL +5 -0
- mfcli-0.2.1.dist-info/entry_points.txt +4 -0
- mfcli-0.2.1.dist-info/licenses/LICENSE +21 -0
- mfcli-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -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
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import chromadb
|
|
2
|
+
from chromadb.utils import embedding_functions
|
|
3
|
+
from typing import Annotated, Optional
|
|
4
|
+
|
|
5
|
+
from mfcli.crud.project import get_project_by_name
|
|
6
|
+
from mfcli.mcp.mcp_instance import mcp
|
|
7
|
+
from mfcli.mcp.state_manager import state_manager
|
|
8
|
+
from mfcli.utils.config import get_config
|
|
9
|
+
from mfcli.utils.directory_manager import app_dirs
|
|
10
|
+
from mfcli.utils.orm import Session
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_chroma_index(index_name: str):
|
|
14
|
+
config = get_config()
|
|
15
|
+
chroma_client = chromadb.PersistentClient(path=app_dirs.chroma_db_dir)
|
|
16
|
+
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
|
|
17
|
+
api_key=config.openai_api_key,
|
|
18
|
+
model_name=config.embedding_model
|
|
19
|
+
)
|
|
20
|
+
return chroma_client.get_collection(index_name, embedding_function=openai_ef)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_index_id_for_project_name(project_name: str):
|
|
24
|
+
with Session() as db:
|
|
25
|
+
project = get_project_by_name(db, project_name)
|
|
26
|
+
return project.index_id
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@mcp.tool()
|
|
30
|
+
def query_local_rag(
|
|
31
|
+
query: Annotated[str, "Search query for engineering documentation (e.g., 'IEC 61000-4-2', 'power management')"],
|
|
32
|
+
project_name: Annotated[Optional[str], "The name of the project. RECOMMENDED: Provide this parameter even though it's optional. The project name can be found in the config.json file located in the .multifactor folder. If not provided, uses the last known project name."] = None,
|
|
33
|
+
n_results: Annotated[int, "Number of results to return (1-20, default: 8)"] = 8,
|
|
34
|
+
task_progress: Annotated[Optional[str], "A checklist showing task progress after this tool use is completed"] = None
|
|
35
|
+
) -> dict:
|
|
36
|
+
"""Query local hardware engineering documentation.
|
|
37
|
+
|
|
38
|
+
Use this tool to search for engineering documentation, design patterns,
|
|
39
|
+
technical specifications, and best practices from the local knowledge base.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
query: Search query for engineering documentation (e.g., "IEC 61000-4-2", "power management")
|
|
43
|
+
project_name: RECOMMENDED - The name of the project. Although optional, it is recommended to provide this parameter.
|
|
44
|
+
The project name can be found in the config.json file located in the .multifactor folder.
|
|
45
|
+
If not provided, uses the last known project name.
|
|
46
|
+
n_results: Number of results to return (1-20, default: 8)
|
|
47
|
+
task_progress: A checklist showing task progress after this tool use is completed
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Dictionary containing:
|
|
51
|
+
- documents: List of text chunks matching the query
|
|
52
|
+
- metadatas: Metadata for each document
|
|
53
|
+
- distances: Similarity distances (lower is more similar)
|
|
54
|
+
- count: Number of results returned
|
|
55
|
+
- chromadb_path: Path to the ChromaDB database
|
|
56
|
+
- project_name: The project name that was used for the query
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
# If project_name is not provided, use the last known project name
|
|
60
|
+
if project_name is None:
|
|
61
|
+
project_name = state_manager.get_last_project_name()
|
|
62
|
+
if project_name is None:
|
|
63
|
+
return {
|
|
64
|
+
"error": "No project name provided and no previous project name found. Please provide a project_name parameter. The project name can be found in the config.json file located in the .multifactor folder.",
|
|
65
|
+
"documents": [],
|
|
66
|
+
"metadatas": [],
|
|
67
|
+
"distances": [],
|
|
68
|
+
"count": 0,
|
|
69
|
+
"chromadb_path": app_dirs.chroma_db_dir,
|
|
70
|
+
"project_name": None
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Store the project name for future use
|
|
74
|
+
state_manager.set_last_project_name(project_name)
|
|
75
|
+
|
|
76
|
+
collection = get_chroma_index(get_index_id_for_project_name(project_name))
|
|
77
|
+
|
|
78
|
+
# Clamp n_results between 1 and 20
|
|
79
|
+
n_results = min(max(n_results, 1), 20)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
results = collection.query(
|
|
83
|
+
query_texts=[query],
|
|
84
|
+
n_results=n_results
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
documents = results["documents"][0] if results["documents"] else []
|
|
88
|
+
metadatas = results["metadatas"][0] if results["metadatas"] else []
|
|
89
|
+
distances = results["distances"][0] if results["distances"] else []
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"documents": documents,
|
|
93
|
+
"metadatas": metadatas,
|
|
94
|
+
"distances": distances,
|
|
95
|
+
"count": len(documents),
|
|
96
|
+
"chromadb_path": app_dirs.chroma_db_dir,
|
|
97
|
+
"project_name": project_name
|
|
98
|
+
}
|
|
99
|
+
except Exception as e:
|
|
100
|
+
return {
|
|
101
|
+
"error": str(e),
|
|
102
|
+
"documents": [],
|
|
103
|
+
"metadatas": [],
|
|
104
|
+
"distances": [],
|
|
105
|
+
"count": 0,
|
|
106
|
+
"chromadb_path": app_dirs.chroma_db_dir,
|
|
107
|
+
"project_name": project_name
|
|
108
|
+
}
|
mfcli/models/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from mfcli.models.file import File
|
|
2
|
+
from mfcli.models.pdf_parts import PDFPart
|
|
3
|
+
from mfcli.models.pipeline_run import PipelineRun
|
|
4
|
+
from mfcli.models.bom import BOM
|
|
5
|
+
from mfcli.models.datasheet import Datasheet
|
|
6
|
+
from mfcli.models.netlist import Netlist
|
|
7
|
+
from mfcli.models.netlist import NetlistPin
|
|
8
|
+
from mfcli.models.netlist import NetlistComponent
|
|
9
|
+
from mfcli.models.project import Project
|
|
10
|
+
from mfcli.models.functional_blocks import FunctionalBlock, FunctionalBlockComponent
|
mfcli/models/base.py
ADDED
mfcli/models/bom.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from sqlalchemy import Column, func, TIMESTAMP
|
|
5
|
+
|
|
6
|
+
from mfcli.models.file import File
|
|
7
|
+
|
|
8
|
+
from sqlmodel import SQLModel, Field, Relationship
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BOMSchema(BaseModel):
|
|
13
|
+
# Required core fields
|
|
14
|
+
reference: str = Field(..., description="Reference designators (C1, R3, U2, etc.)")
|
|
15
|
+
value: str = Field(..., description="Electrical value or part name (10kΩ, MSPM0L1306)")
|
|
16
|
+
quantity: int = Field(..., description="Quantity of identical components")
|
|
17
|
+
description: str = Field(..., description="General text describing component")
|
|
18
|
+
|
|
19
|
+
# Optional fields
|
|
20
|
+
manufacturer: Optional[str] = Field(None, description="Manufacturer name")
|
|
21
|
+
mpn: Optional[str] = Field(None, description="Manufacturer part number")
|
|
22
|
+
lifecycle_status: Optional[str] = Field(None, description="Maturity (Active / NRND / Obsolete)")
|
|
23
|
+
supplier: Optional[str] = Field(None, description="Distributor / supplier")
|
|
24
|
+
supplier_part: Optional[str] = Field(None, description="Supplier SKU or catalog number")
|
|
25
|
+
supplier_unit_price: Optional[float] = Field(None, description="Unit cost")
|
|
26
|
+
supplier_subtotal: Optional[float] = Field(None, description="Quantity × unit cost")
|
|
27
|
+
revision_id: Optional[str] = Field(None, description="Design revision identifier")
|
|
28
|
+
revision_state: Optional[str] = Field(None, description="Engineering state (Released / Draft)")
|
|
29
|
+
revision_status: Optional[str] = Field(None, description="Workflow status (Approved / Pending)")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BOM(SQLModel, table=True):
|
|
33
|
+
__tablename__ = "bom"
|
|
34
|
+
|
|
35
|
+
id: Optional[int] = Field(default=None, primary_key=True)
|
|
36
|
+
file_id: int = Field(
|
|
37
|
+
foreign_key="files.id",
|
|
38
|
+
index=True,
|
|
39
|
+
nullable=False,
|
|
40
|
+
ondelete="CASCADE"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Relationship
|
|
44
|
+
file: Optional["File"] = Relationship()
|
|
45
|
+
|
|
46
|
+
# Required core fields
|
|
47
|
+
reference: str = Field(max_length=100, nullable=False)
|
|
48
|
+
value: str = Field(max_length=255, nullable=False)
|
|
49
|
+
quantity: int = Field(nullable=False)
|
|
50
|
+
description: str = Field(max_length=600, nullable=False)
|
|
51
|
+
|
|
52
|
+
# Optional fields
|
|
53
|
+
manufacturer: Optional[str] = Field(default=None, max_length=255)
|
|
54
|
+
mpn: Optional[str] = Field(default=None, max_length=255)
|
|
55
|
+
lifecycle_status: Optional[str] = Field(default=None, max_length=50)
|
|
56
|
+
supplier: Optional[str] = Field(default=None, max_length=255)
|
|
57
|
+
supplier_part: Optional[str] = Field(default=None, max_length=255)
|
|
58
|
+
supplier_unit_price: Optional[float] = Field(default=None)
|
|
59
|
+
supplier_subtotal: Optional[float] = Field(default=None)
|
|
60
|
+
revision_id: Optional[str] = Field(default=None, max_length=50)
|
|
61
|
+
revision_state: Optional[str] = Field(default=None, max_length=50)
|
|
62
|
+
revision_status: Optional[str] = Field(default=None, max_length=100)
|
|
63
|
+
datasheet: Optional[str] = Field(default=None, nullable=True, max_length=500)
|
|
64
|
+
|
|
65
|
+
# Timestamps
|
|
66
|
+
created_at: datetime = Field(
|
|
67
|
+
sa_column=Column(TIMESTAMP(timezone=True), server_default=func.now(), nullable=False)
|
|
68
|
+
)
|
|
69
|
+
updated_at: datetime = Field(
|
|
70
|
+
sa_column=Column(TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
|
71
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from sqlmodel import SQLModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Datasheet(SQLModel, table=True):
|
|
7
|
+
__tablename__ = "datasheets"
|
|
8
|
+
id: Optional[int] = Field(default=None, primary_key=True)
|
|
9
|
+
part_number: str = Field(index=True, nullable=False, max_length=100, unique=False)
|
|
10
|
+
datasheet: str = Field(index=True, nullable=False, max_length=500, unique=False)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from typing import Dict, List, Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DebugInterface(BaseModel):
|
|
7
|
+
"""Debug interface configuration from schematic."""
|
|
8
|
+
interface_type: str = Field(..., description="Debug interface type: JTAG, SWD, ICSP, etc.")
|
|
9
|
+
pin_count: int = Field(0, description="Number of pins in debug connector")
|
|
10
|
+
pin_mapping: Dict[str, str] = Field(default_factory=dict,
|
|
11
|
+
description="Pin number to signal mapping (e.g., {'1': 'VCC', '2': 'SWDIO'})")
|
|
12
|
+
connector_type: str = Field("", description="Physical connector type (e.g., 10-pin Cortex, 20-pin JTAG)")
|
|
13
|
+
voltage_level: str = Field("", description="Target voltage level (3.3V, 5V, etc.)")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DebugTool(BaseModel):
|
|
17
|
+
"""Recommended debug tool/probe."""
|
|
18
|
+
name: str = Field(..., description="Tool name (e.g., J-Link, ST-Link, CMSIS-DAP)")
|
|
19
|
+
model: str = Field("", description="Specific model recommendation")
|
|
20
|
+
purchase_links: List[str] = Field(default_factory=list, description="Where to buy")
|
|
21
|
+
compatibility_notes: str = Field("", description="Why this tool is recommended")
|
|
22
|
+
alternative_tools: List[str] = Field(default_factory=list, description="Alternative compatible tools")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DriverInfo(BaseModel):
|
|
26
|
+
"""Driver installation information."""
|
|
27
|
+
windows: Dict[str, Any] = Field(default_factory=dict,
|
|
28
|
+
description="Windows driver info: download_url, installation_steps")
|
|
29
|
+
linux: Dict[str, Any] = Field(default_factory=dict,
|
|
30
|
+
description="Linux driver info: package_name, installation_steps")
|
|
31
|
+
macos: Dict[str, Any] = Field(default_factory=dict, description="macOS driver info (optional)")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class VSCodeLaunchConfig(BaseModel):
|
|
35
|
+
"""VS Code launch.json configuration."""
|
|
36
|
+
configuration_name: str = Field(..., description="Name for this configuration")
|
|
37
|
+
configuration_type: str = Field(..., description="Debugger type: cortex-debug, cppdbg, etc.")
|
|
38
|
+
launch_json: Dict[str, Any] = Field(default_factory=dict, description="Complete launch.json configuration")
|
|
39
|
+
extensions_required: List[str] = Field(default_factory=list, description="Required VS Code extensions")
|
|
40
|
+
setup_notes: str = Field("", description="Additional setup notes")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class GDBServerConfig(BaseModel):
|
|
44
|
+
"""GDB server configuration."""
|
|
45
|
+
server_name: str = Field(..., description="GDB server name (e.g., JLinkGDBServer, OpenOCD)")
|
|
46
|
+
command_line: str = Field("", description="Command to start GDB server")
|
|
47
|
+
config_file: str = Field("", description="Configuration file content or path")
|
|
48
|
+
port: int = Field(3333, description="GDB server port")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DSTargetMCU(BaseModel):
|
|
52
|
+
target_mcu: str = Field(..., description="Target MCU from schematic")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class DSAdditionalTools(BaseModel):
|
|
56
|
+
additional_tools: List[str] = Field(default_factory=list, description="Additional helpful tools")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class DSTroubleshootingGuide(BaseModel):
|
|
60
|
+
troubleshooting: List[str] = Field(default_factory=list, description="Common issues and solutions")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DSQuickStartGuide(BaseModel):
|
|
64
|
+
quick_start_guide: List[str] = Field(default_factory=list, description="Step-by-step quick start")
|
mfcli/models/file.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import func, UniqueConstraint, Column, DateTime
|
|
5
|
+
from sqlmodel import SQLModel, Field, Relationship
|
|
6
|
+
|
|
7
|
+
from mfcli.models.pipeline_run import PipelineRun
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class File(SQLModel, table=True):
|
|
11
|
+
__tablename__ = "files"
|
|
12
|
+
|
|
13
|
+
__table_args__ = (
|
|
14
|
+
UniqueConstraint("md5", "pipeline_run_id", name="uq_file_md5_pipeline_run"),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
id: Optional[int] = Field(default=None, primary_key=True)
|
|
18
|
+
pipeline_run_id: int = Field(
|
|
19
|
+
foreign_key="pipeline_runs.id",
|
|
20
|
+
index=True,
|
|
21
|
+
nullable=False,
|
|
22
|
+
ondelete="CASCADE"
|
|
23
|
+
)
|
|
24
|
+
pipeline_run: Optional["PipelineRun"] = Relationship()
|
|
25
|
+
pdf_parts: List["PDFPart"] = Relationship(back_populates='file', cascade_delete=True)
|
|
26
|
+
|
|
27
|
+
name: str = Field(max_length=255, nullable=False, index=True)
|
|
28
|
+
type: int = Field(nullable=False, index=True)
|
|
29
|
+
sub_type: Optional[int] = Field(default=None, index=True)
|
|
30
|
+
mime_type: str = Field(max_length=255, nullable=False)
|
|
31
|
+
md5: str = Field(max_length=32, nullable=False, index=True)
|
|
32
|
+
ext: str = Field(max_length=10, min_length=2, index=True, nullable=True)
|
|
33
|
+
path: str = Field(max_length=600, nullable=True)
|
|
34
|
+
gemini_file_id: str = Field(max_length=40, nullable=True)
|
|
35
|
+
is_datasheet: int = Field(default=0, nullable=False, index=True)
|
|
36
|
+
|
|
37
|
+
# Database-managed timestamps
|
|
38
|
+
created_at: datetime = Field(
|
|
39
|
+
sa_column=Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
40
|
+
)
|
|
41
|
+
updated_at: datetime = Field(
|
|
42
|
+
sa_column=Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
|
43
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FileDocketEntry(BaseModel):
|
|
9
|
+
name: str
|
|
10
|
+
path: str
|
|
11
|
+
vectorize: bool
|
|
12
|
+
sub_type: str
|
|
13
|
+
md5: str | None = Field(default=None)
|
|
14
|
+
is_datasheet: bool = Field(default=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FileDocketEntries(BaseModel):
|
|
18
|
+
entries: List[FileDocketEntry] = Field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
def add(self, entry: FileDocketEntry):
|
|
21
|
+
self.entries.append(entry)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FileDocket:
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self._docket = FileDocketEntries()
|
|
27
|
+
self._md5_entry_map: Dict[str, FileDocketEntry] = {}
|
|
28
|
+
self._path_entry_map: Dict[str, FileDocketEntry] = {}
|
|
29
|
+
|
|
30
|
+
def add(self, entry: FileDocketEntry):
|
|
31
|
+
self._docket.add(entry)
|
|
32
|
+
if entry.md5:
|
|
33
|
+
self._md5_entry_map[entry.md5] = entry
|
|
34
|
+
# Use normalized path as key (convert to lowercase for case-insensitive comparison on Windows)
|
|
35
|
+
normalized_path = Path(entry.path).as_posix().lower()
|
|
36
|
+
self._path_entry_map[normalized_path] = entry
|
|
37
|
+
|
|
38
|
+
def get_by_md5(self, md5: str) -> Optional[FileDocketEntry]:
|
|
39
|
+
return self._md5_entry_map.get(md5)
|
|
40
|
+
|
|
41
|
+
def get_by_path(self, path: str) -> Optional[FileDocketEntry]:
|
|
42
|
+
normalized_path = Path(path).as_posix().lower()
|
|
43
|
+
return self._path_entry_map.get(normalized_path)
|
|
44
|
+
|
|
45
|
+
def remove(self, entry: FileDocketEntry):
|
|
46
|
+
"""Remove an entry from the docket"""
|
|
47
|
+
if entry in self._docket.entries:
|
|
48
|
+
self._docket.entries.remove(entry)
|
|
49
|
+
if entry.md5 and entry.md5 in self._md5_entry_map:
|
|
50
|
+
del self._md5_entry_map[entry.md5]
|
|
51
|
+
normalized_path = Path(entry.path).as_posix().lower()
|
|
52
|
+
if normalized_path in self._path_entry_map:
|
|
53
|
+
del self._path_entry_map[normalized_path]
|
|
54
|
+
|
|
55
|
+
def load_from_json(self, json_path: Path):
|
|
56
|
+
"""Load existing file docket from JSON file"""
|
|
57
|
+
if not json_path.exists():
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
with open(json_path, 'r') as f:
|
|
62
|
+
data = json.load(f)
|
|
63
|
+
|
|
64
|
+
# Convert the saved format back to entries
|
|
65
|
+
all_entries = []
|
|
66
|
+
for category in ['hw_files', 'datasheets', 'cheat_sheets']:
|
|
67
|
+
if category in data:
|
|
68
|
+
all_entries.extend(data[category])
|
|
69
|
+
|
|
70
|
+
# Create FileDocketEntry objects and add them
|
|
71
|
+
for entry_data in all_entries:
|
|
72
|
+
entry = FileDocketEntry(**entry_data)
|
|
73
|
+
self.add(entry)
|
|
74
|
+
|
|
75
|
+
except Exception as e:
|
|
76
|
+
# If there's an error loading, just start fresh
|
|
77
|
+
print(f"Warning: Could not load existing file_docket.json: {e}")
|
|
78
|
+
|
|
79
|
+
def get_entries(self) -> Dict:
|
|
80
|
+
datasheets = []
|
|
81
|
+
cheat_sheets = []
|
|
82
|
+
hw_files = []
|
|
83
|
+
for entry in self._docket.entries:
|
|
84
|
+
if entry.is_datasheet:
|
|
85
|
+
datasheets.append(entry.model_dump())
|
|
86
|
+
elif entry.sub_type == "CHEAT_SHEET":
|
|
87
|
+
cheat_sheets.append(entry.model_dump())
|
|
88
|
+
else:
|
|
89
|
+
hw_files.append(entry.model_dump())
|
|
90
|
+
return {
|
|
91
|
+
"hw_files": hw_files,
|
|
92
|
+
"datasheets": datasheets,
|
|
93
|
+
"cheat_sheets": cheat_sheets
|
|
94
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from mfcli.constants.file_types import FileSubtypes, FileTypes
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FileMetadata(BaseModel):
|
|
9
|
+
name: str
|
|
10
|
+
size: int
|
|
11
|
+
md5: str
|
|
12
|
+
path: str
|
|
13
|
+
mime: str
|
|
14
|
+
ext: str
|
|
15
|
+
type_id: FileTypes
|
|
16
|
+
type_name: str
|
|
17
|
+
subtype_id: Optional[FileSubtypes] = Field(None)
|
|
18
|
+
subtype_name: Optional[str] = Field(None)
|
|
19
|
+
is_datasheet: bool
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from sqlmodel import SQLModel, Field as DBField, Relationship
|
|
5
|
+
|
|
6
|
+
from mfcli.models.pipeline_run import PipelineRun
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# DB Models
|
|
10
|
+
|
|
11
|
+
class FunctionalBlock(SQLModel, table=True):
|
|
12
|
+
__tablename__ = 'functional_blocks'
|
|
13
|
+
id: Optional[int] = DBField(default=None, primary_key=True)
|
|
14
|
+
pipeline_run_id: int = DBField(
|
|
15
|
+
foreign_key="pipeline_runs.id",
|
|
16
|
+
index=True,
|
|
17
|
+
nullable=False,
|
|
18
|
+
ondelete="CASCADE"
|
|
19
|
+
)
|
|
20
|
+
pipeline_run: Optional["PipelineRun"] = Relationship()
|
|
21
|
+
name: str = DBField(max_length=255, nullable=False, index=True)
|
|
22
|
+
description: str = DBField(nullable=False)
|
|
23
|
+
components: List["FunctionalBlockComponent"] = Relationship(back_populates='functional_block', cascade_delete=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FunctionalBlockComponent(SQLModel, table=True):
|
|
27
|
+
__tablename__ = 'functional_block_components'
|
|
28
|
+
id: Optional[int] = DBField(default=None, primary_key=True)
|
|
29
|
+
functional_block_id: int = DBField(
|
|
30
|
+
foreign_key="functional_blocks.id",
|
|
31
|
+
index=True,
|
|
32
|
+
nullable=False,
|
|
33
|
+
ondelete="CASCADE"
|
|
34
|
+
)
|
|
35
|
+
functional_block: Optional["FunctionalBlock"] = Relationship(back_populates='components')
|
|
36
|
+
ref: str = DBField(max_length=45, nullable=False)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class FunctionalBlocks(BaseModel):
|
|
40
|
+
functional_blocks: list[str] = Field(..., description="Names of functional blocks")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Pydantic models
|
|
44
|
+
|
|
45
|
+
class PinInfo(BaseModel):
|
|
46
|
+
"""Represents pin configuration information."""
|
|
47
|
+
pin_number: str = Field(..., description="Pin number or identifier")
|
|
48
|
+
pin_name: str = Field(..., description="Pin name or signal name")
|
|
49
|
+
direction: str = Field("", description="Pin direction: input, output, bidirectional, power, ground")
|
|
50
|
+
pull_config: str = Field("", description="Pull-up/pull-down configuration: pull-up, pull-down, none, external")
|
|
51
|
+
voltage_level: str = Field("", description="Voltage level (e.g., 3.3V, 5V, 1.8V)")
|
|
52
|
+
function: str = Field("", description="Pin function description")
|
|
53
|
+
connected_to: str = Field("", description="What this pin connects to")
|
|
54
|
+
alternate_functions: List[str] = Field(default_factory=list, description="Alternative pin functions")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ConnectorInfo(BaseModel):
|
|
58
|
+
"""Represents connector information."""
|
|
59
|
+
connector_ref: str = Field(..., description="Connector reference designator (e.g., J1, P2)")
|
|
60
|
+
connector_type: str = Field("", description="Connector type (e.g., USB-C, JST-XH, header)")
|
|
61
|
+
pin_count: int = Field(0, description="Number of pins in connector")
|
|
62
|
+
pins: List[PinInfo] = Field(default_factory=list, description="Pin information for connector")
|
|
63
|
+
description: str = Field("", description="Connector description and purpose")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class FunctionalBlockMeta(BaseModel):
|
|
67
|
+
name: str = Field(..., description="Name of the functional block (e.g., 'Power Supply', 'USB Interface')")
|
|
68
|
+
description: str = Field(..., description="Detailed description of what this block does")
|
|
69
|
+
components: List[str] = Field(default_factory=list, description="Reference designators of components in this block")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class FBPins(BaseModel):
|
|
73
|
+
pins: List[PinInfo] = Field(default_factory=list, description="Pin information for this functional block")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class FBConnectors(BaseModel):
|
|
77
|
+
connectors: List[ConnectorInfo] = Field(default_factory=list, description="Connectors associated with this block")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class FBVoltageRails(BaseModel):
|
|
81
|
+
voltage_rails: List[str] = Field(default_factory=list,
|
|
82
|
+
description="Voltage rails used by this block (e.g., 3.3V, 5V)")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class FBInitializationNotes(BaseModel):
|
|
86
|
+
initialization_notes: str = Field("", description="Notes on how to initialize/configure this block in firmware")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class FBDependencies(BaseModel):
|
|
90
|
+
dependencies: List[str] = Field(default_factory=list, description="Other functional blocks this depends on")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class FBFirmwareRequirements(BaseModel):
|
|
94
|
+
firmware_requirements: str = Field("", description="Key firmware requirements for this block")
|