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.
Files changed (138) hide show
  1. mfcli/.env.example +72 -0
  2. mfcli/__init__.py +0 -0
  3. mfcli/agents/__init__.py +0 -0
  4. mfcli/agents/controller/__init__.py +0 -0
  5. mfcli/agents/controller/agent.py +19 -0
  6. mfcli/agents/controller/config.yaml +27 -0
  7. mfcli/agents/controller/tools.py +42 -0
  8. mfcli/agents/tools/general.py +118 -0
  9. mfcli/alembic/env.py +61 -0
  10. mfcli/alembic/script.py.mako +28 -0
  11. mfcli/alembic/versions/6ccc0c7c397c_added_fields_to_pdf_parts_model.py +39 -0
  12. mfcli/alembic/versions/769019ef4870_added_gemini_file_path_to_pdf_part_model.py +33 -0
  13. mfcli/alembic/versions/7a2e3a779fdc_added_functional_block_and_component_.py +54 -0
  14. mfcli/alembic/versions/7d5adb2a47a7_added_pdf_parts_model.py +41 -0
  15. mfcli/alembic/versions/7fcb7d6a5836_init.py +167 -0
  16. mfcli/alembic/versions/e0f2b5765c72_added_cascade_delete_for_models_that_.py +32 -0
  17. mfcli/alembic.ini +147 -0
  18. mfcli/cli/__init__.py +0 -0
  19. mfcli/cli/dependencies.py +59 -0
  20. mfcli/cli/main.py +200 -0
  21. mfcli/client/__init__.py +0 -0
  22. mfcli/client/chroma_db.py +184 -0
  23. mfcli/client/docling.py +44 -0
  24. mfcli/client/gemini.py +252 -0
  25. mfcli/client/llama_parse.py +38 -0
  26. mfcli/client/vector_db.py +93 -0
  27. mfcli/constants/__init__.py +0 -0
  28. mfcli/constants/base_enum.py +18 -0
  29. mfcli/constants/directory_names.py +1 -0
  30. mfcli/constants/file_types.py +189 -0
  31. mfcli/constants/gemini.py +1 -0
  32. mfcli/constants/openai.py +6 -0
  33. mfcli/constants/pipeline_run_status.py +3 -0
  34. mfcli/crud/__init__.py +0 -0
  35. mfcli/crud/file.py +42 -0
  36. mfcli/crud/functional_blocks.py +26 -0
  37. mfcli/crud/netlist.py +18 -0
  38. mfcli/crud/pipeline_run.py +17 -0
  39. mfcli/crud/project.py +144 -0
  40. mfcli/digikey/__init__.py +0 -0
  41. mfcli/digikey/digikey.py +105 -0
  42. mfcli/main.py +5 -0
  43. mfcli/mcp/__init__.py +0 -0
  44. mfcli/mcp/configs/cline_mcp_settings.json +11 -0
  45. mfcli/mcp/configs/mfcli.mcp.json +7 -0
  46. mfcli/mcp/mcp_instance.py +6 -0
  47. mfcli/mcp/server.py +37 -0
  48. mfcli/mcp/state_manager.py +51 -0
  49. mfcli/mcp/tools/__init__.py +0 -0
  50. mfcli/mcp/tools/query_knowledgebase.py +108 -0
  51. mfcli/models/__init__.py +10 -0
  52. mfcli/models/base.py +10 -0
  53. mfcli/models/bom.py +71 -0
  54. mfcli/models/datasheet.py +10 -0
  55. mfcli/models/debug_setup.py +64 -0
  56. mfcli/models/file.py +43 -0
  57. mfcli/models/file_docket.py +94 -0
  58. mfcli/models/file_metadata.py +19 -0
  59. mfcli/models/functional_blocks.py +94 -0
  60. mfcli/models/llm_response.py +5 -0
  61. mfcli/models/mcu.py +97 -0
  62. mfcli/models/mcu_errata.py +26 -0
  63. mfcli/models/netlist.py +59 -0
  64. mfcli/models/pdf_parts.py +25 -0
  65. mfcli/models/pipeline_run.py +34 -0
  66. mfcli/models/project.py +27 -0
  67. mfcli/models/project_metadata.py +15 -0
  68. mfcli/pipeline/__init__.py +0 -0
  69. mfcli/pipeline/analysis/__init__.py +0 -0
  70. mfcli/pipeline/analysis/bom_netlist_mapper.py +28 -0
  71. mfcli/pipeline/analysis/generators/__init__.py +0 -0
  72. mfcli/pipeline/analysis/generators/bom/__init__.py +0 -0
  73. mfcli/pipeline/analysis/generators/bom/bom.py +74 -0
  74. mfcli/pipeline/analysis/generators/debug_setup/__init__.py +0 -0
  75. mfcli/pipeline/analysis/generators/debug_setup/debug_setup.py +71 -0
  76. mfcli/pipeline/analysis/generators/debug_setup/instructions.py +150 -0
  77. mfcli/pipeline/analysis/generators/functional_blocks/__init__.py +0 -0
  78. mfcli/pipeline/analysis/generators/functional_blocks/functional_blocks.py +93 -0
  79. mfcli/pipeline/analysis/generators/functional_blocks/instructions.py +34 -0
  80. mfcli/pipeline/analysis/generators/functional_blocks/validator.py +94 -0
  81. mfcli/pipeline/analysis/generators/generator.py +258 -0
  82. mfcli/pipeline/analysis/generators/generator_base.py +18 -0
  83. mfcli/pipeline/analysis/generators/mcu/__init__.py +0 -0
  84. mfcli/pipeline/analysis/generators/mcu/instructions.py +156 -0
  85. mfcli/pipeline/analysis/generators/mcu/mcu.py +84 -0
  86. mfcli/pipeline/analysis/generators/mcu_errata/__init__.py +1 -0
  87. mfcli/pipeline/analysis/generators/mcu_errata/instructions.py +77 -0
  88. mfcli/pipeline/analysis/generators/mcu_errata/mcu_errata.py +95 -0
  89. mfcli/pipeline/analysis/generators/summary/__init__.py +0 -0
  90. mfcli/pipeline/analysis/generators/summary/summary.py +47 -0
  91. mfcli/pipeline/classifier.py +93 -0
  92. mfcli/pipeline/data_enricher.py +15 -0
  93. mfcli/pipeline/extractor.py +34 -0
  94. mfcli/pipeline/extractors/__init__.py +0 -0
  95. mfcli/pipeline/extractors/pdf.py +12 -0
  96. mfcli/pipeline/parser.py +120 -0
  97. mfcli/pipeline/parsers/__init__.py +0 -0
  98. mfcli/pipeline/parsers/netlist/__init__.py +0 -0
  99. mfcli/pipeline/parsers/netlist/edif.py +93 -0
  100. mfcli/pipeline/parsers/netlist/kicad_legacy_net.py +326 -0
  101. mfcli/pipeline/parsers/netlist/kicad_spice.py +135 -0
  102. mfcli/pipeline/parsers/netlist/pads.py +185 -0
  103. mfcli/pipeline/parsers/netlist/protel.py +166 -0
  104. mfcli/pipeline/parsers/netlist/protel_detector.py +29 -0
  105. mfcli/pipeline/pipeline.py +470 -0
  106. mfcli/pipeline/preprocessors/__init__.py +0 -0
  107. mfcli/pipeline/preprocessors/user_guide.py +127 -0
  108. mfcli/pipeline/run_context.py +32 -0
  109. mfcli/pipeline/schema_mapper.py +89 -0
  110. mfcli/pipeline/sub_classifier.py +115 -0
  111. mfcli/utils/__init__.py +0 -0
  112. mfcli/utils/cline_rules.py +256 -0
  113. mfcli/utils/config.py +33 -0
  114. mfcli/utils/configurator.py +324 -0
  115. mfcli/utils/data_cleaner.py +114 -0
  116. mfcli/utils/datasheet_vectorizer.py +283 -0
  117. mfcli/utils/directory_manager.py +116 -0
  118. mfcli/utils/file_upload.py +298 -0
  119. mfcli/utils/files.py +16 -0
  120. mfcli/utils/http_requests.py +54 -0
  121. mfcli/utils/kb_lister.py +89 -0
  122. mfcli/utils/kb_remover.py +173 -0
  123. mfcli/utils/logger.py +28 -0
  124. mfcli/utils/mcp_configurator.py +394 -0
  125. mfcli/utils/migrations.py +18 -0
  126. mfcli/utils/orm.py +43 -0
  127. mfcli/utils/pdf_splitter.py +63 -0
  128. mfcli/utils/pre_uninstall.py +167 -0
  129. mfcli/utils/query_service.py +22 -0
  130. mfcli/utils/system_check.py +306 -0
  131. mfcli/utils/tools.py +98 -0
  132. mfcli/utils/vectorizer.py +28 -0
  133. mfcli-0.2.1.dist-info/METADATA +956 -0
  134. mfcli-0.2.1.dist-info/RECORD +138 -0
  135. mfcli-0.2.1.dist-info/WHEEL +5 -0
  136. mfcli-0.2.1.dist-info/entry_points.txt +4 -0
  137. mfcli-0.2.1.dist-info/licenses/LICENSE +21 -0
  138. mfcli-0.2.1.dist-info/top_level.txt +1 -0
mfcli/utils/files.py ADDED
@@ -0,0 +1,16 @@
1
+ import os
2
+ import re
3
+ from pathlib import Path
4
+ from typing import Literal
5
+
6
+ AppDataFileTypes = Literal['datasheets', 'files']
7
+
8
+
9
+ def file_access_check(file_path: Path) -> bool:
10
+ if not os.path.exists(file_path) or not os.access(file_path, os.R_OK):
11
+ return False
12
+ return True
13
+
14
+
15
+ def is_text_mime_type(mime_type: str) -> bool:
16
+ return bool(re.match(r"^text/.+$", mime_type))
@@ -0,0 +1,54 @@
1
+ import requests
2
+ from requests.adapters import HTTPAdapter
3
+ from urllib3.util.retry import Retry
4
+
5
+
6
+ def get_retry_session(
7
+ retries: int = 3,
8
+ backoff_factor: float = 0.5,
9
+ status_forcelist: tuple = (500, 502, 503, 504),
10
+ allowed_methods: tuple = ("GET", "POST", "PUT", "DELETE", "PATCH")
11
+ ) -> requests.Session:
12
+ """Create a requests session with retry logic."""
13
+ session = requests.Session()
14
+ retry = Retry(
15
+ total=retries,
16
+ read=retries,
17
+ connect=retries,
18
+ backoff_factor=backoff_factor,
19
+ status_forcelist=status_forcelist,
20
+ allowed_methods=allowed_methods,
21
+ raise_on_status=False,
22
+ )
23
+ adapter = HTTPAdapter(max_retries=retry)
24
+ session.mount("http://", adapter)
25
+ session.mount("https://", adapter)
26
+ return session
27
+
28
+
29
+ def http_request(
30
+ method: str,
31
+ url: str,
32
+ retries: int = 3,
33
+ timeout: int = 10,
34
+ **kwargs
35
+ ) -> requests.Response:
36
+ """
37
+ Perform an HTTP request with automatic retry logic.
38
+
39
+ :param method: HTTP method (GET, POST, PUT, DELETE, etc.)
40
+ :param url: URL to request
41
+ :param retries: Number of retry attempts
42
+ :param timeout: Timeout per request in seconds
43
+ :param kwargs: Any requests.request() parameters (headers, data, json, etc.)
44
+ :return: requests.Response object
45
+ """
46
+ session = get_retry_session(retries=retries)
47
+ try:
48
+ response = session.request(method.upper(), url, allow_redirects=True, timeout=timeout, **kwargs)
49
+ response.raise_for_status()
50
+ return response
51
+ except requests.exceptions.RequestException:
52
+ raise
53
+ finally:
54
+ session.close()
@@ -0,0 +1,89 @@
1
+ from typing import Dict, List, Set
2
+ from mfcli.client.chroma_db import get_chromadb_client_for_project_name
3
+ from mfcli.utils.logger import get_logger
4
+ from mfcli.utils.orm import Session
5
+
6
+ logger = get_logger(__name__)
7
+
8
+
9
+ def list_vectorized_files(project_name: str) -> Dict[str, List[str]]:
10
+ """
11
+ List all files that have been vectorized into the ChromaDB database for a project.
12
+
13
+ Args:
14
+ project_name: Name of the project
15
+
16
+ Returns:
17
+ Dictionary mapping purpose to list of file names
18
+ """
19
+ try:
20
+ with Session() as db:
21
+ chroma_client = get_chromadb_client_for_project_name(db, project_name)
22
+
23
+ # Get all documents from the collection
24
+ collection = chroma_client._collection
25
+ results = collection.get()
26
+
27
+ if not results or not results.get('metadatas'):
28
+ logger.info("No vectorized files found in the knowledge base")
29
+ return {}
30
+
31
+ # Group files by purpose
32
+ files_by_purpose: Dict[str, Set[str]] = {}
33
+
34
+ for metadata in results['metadatas']:
35
+ if metadata:
36
+ file_name = metadata.get('file_name', 'Unknown')
37
+ purpose = metadata.get('purpose', 'unknown')
38
+
39
+ if purpose not in files_by_purpose:
40
+ files_by_purpose[purpose] = set()
41
+ files_by_purpose[purpose].add(file_name)
42
+
43
+ # Convert sets to sorted lists for consistent output
44
+ result = {
45
+ purpose: sorted(list(files))
46
+ for purpose, files in files_by_purpose.items()
47
+ }
48
+
49
+ return result
50
+
51
+ except Exception as e:
52
+ logger.error(f"Failed to list vectorized files for project: {project_name}")
53
+ logger.exception(e)
54
+ raise
55
+
56
+
57
+ def print_vectorized_files(project_name: str) -> None:
58
+ """
59
+ Print all vectorized files in a formatted manner.
60
+
61
+ Args:
62
+ project_name: Name of the project
63
+ """
64
+ try:
65
+ files_by_purpose = list_vectorized_files(project_name)
66
+
67
+ if not files_by_purpose:
68
+ print(f"\nNo files have been vectorized for project '{project_name}' yet.")
69
+ return
70
+
71
+ print(f"\nVectorized files in knowledge base for project '{project_name}':")
72
+ print("=" * 70)
73
+
74
+ total_files = 0
75
+ for purpose in sorted(files_by_purpose.keys()):
76
+ files = files_by_purpose[purpose]
77
+ print(f"\n{purpose.upper()} ({len(files)} file{'s' if len(files) != 1 else ''}):")
78
+ print("-" * 70)
79
+ for file_name in files:
80
+ print(f" • {file_name}")
81
+ total_files += len(files)
82
+
83
+ print("\n" + "=" * 70)
84
+ print(f"Total: {total_files} unique file{'s' if total_files != 1 else ''} vectorized\n")
85
+
86
+ except Exception as e:
87
+ logger.error(f"Failed to print vectorized files for project: {project_name}")
88
+ print(f"\nError: Failed to list vectorized files. Check logs for details.")
89
+ raise
@@ -0,0 +1,173 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import List
4
+ from mfcli.client.chroma_db import get_chromadb_client_for_project_name
5
+ from mfcli.models.file_docket import FileDocket
6
+ from mfcli.utils.directory_manager import app_dirs
7
+ from mfcli.utils.logger import get_logger
8
+ from mfcli.utils.orm import Session
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ def _remove_from_file_docket(matching_files: set) -> int:
14
+ """
15
+ Remove files from the file_docket.json based on file names.
16
+
17
+ Args:
18
+ matching_files: Set of file names to remove
19
+
20
+ Returns:
21
+ Number of entries removed from docket
22
+ """
23
+ if not app_dirs.file_docket_path or not app_dirs.file_docket_path.exists():
24
+ logger.warning("file_docket.json not found, skipping docket removal")
25
+ return 0
26
+
27
+ try:
28
+ # Load existing file docket
29
+ docket = FileDocket()
30
+ docket.load_from_json(app_dirs.file_docket_path)
31
+
32
+ # Track how many entries we remove
33
+ removed_count = 0
34
+
35
+ # Find and remove matching entries
36
+ entries_to_remove = []
37
+ for entry in docket._docket.entries:
38
+ if entry.name in matching_files:
39
+ entries_to_remove.append(entry)
40
+
41
+ for entry in entries_to_remove:
42
+ docket.remove(entry)
43
+ removed_count += 1
44
+ logger.debug(f"Removed from docket: {entry.name}")
45
+
46
+ # Save updated docket back to file
47
+ if removed_count > 0:
48
+ json_data = json.dumps(docket.get_entries(), indent=2)
49
+ with open(app_dirs.file_docket_path, "w") as f:
50
+ f.write(json_data)
51
+ logger.info(f"Updated file_docket.json, removed {removed_count} entries")
52
+
53
+ return removed_count
54
+
55
+ except Exception as e:
56
+ logger.error(f"Failed to remove files from file_docket: {e}")
57
+ logger.exception(e)
58
+ return 0
59
+
60
+
61
+ def remove_files_from_kb(project_name: str, filename_pattern: str, confirm: bool = True) -> int:
62
+ """
63
+ Remove files from the ChromaDB knowledge base that match the given filename pattern.
64
+
65
+ Args:
66
+ project_name: Name of the project
67
+ filename_pattern: Full or partial filename to match (case-insensitive)
68
+ confirm: If True, ask for confirmation before deleting
69
+
70
+ Returns:
71
+ Number of chunks deleted
72
+ """
73
+ try:
74
+ with Session() as db:
75
+ chroma_client = get_chromadb_client_for_project_name(db, project_name)
76
+
77
+ # Get all documents from the collection
78
+ collection = chroma_client._collection
79
+ results = collection.get()
80
+
81
+ if not results or not results.get('metadatas'):
82
+ logger.info("No files found in the knowledge base")
83
+ print("No files found in the knowledge base.")
84
+ return 0
85
+
86
+ # Find matching file chunks
87
+ matching_ids: List[str] = []
88
+ matching_files: set = set()
89
+
90
+ for idx, metadata in enumerate(results['metadatas']):
91
+ if metadata:
92
+ file_name = metadata.get('file_name', '')
93
+ # Case-insensitive partial match
94
+ if filename_pattern.lower() in file_name.lower():
95
+ matching_ids.append(results['ids'][idx])
96
+ matching_files.add(file_name)
97
+
98
+ if not matching_ids:
99
+ print(f"\nNo files matching '{filename_pattern}' found in the knowledge base.")
100
+ return 0
101
+
102
+ # Display matching files
103
+ print(f"\nFound {len(matching_files)} file(s) matching '{filename_pattern}':")
104
+ for file_name in sorted(matching_files):
105
+ print(f" • {file_name}")
106
+ print(f"\nTotal chunks to delete: {len(matching_ids)}")
107
+
108
+ # Confirm deletion
109
+ if confirm:
110
+ response = input("\nAre you sure you want to delete these files? (yes/no): ")
111
+ if response.lower() not in ['yes', 'y']:
112
+ print("Deletion cancelled.")
113
+ return 0
114
+
115
+ # Delete the chunks from ChromaDB
116
+ collection.delete(ids=matching_ids)
117
+
118
+ # Remove files from file_docket
119
+ removed_from_docket = _remove_from_file_docket(matching_files)
120
+
121
+ print(f"\nSuccessfully deleted {len(matching_ids)} chunks from {len(matching_files)} file(s).")
122
+ if removed_from_docket > 0:
123
+ print(f"Removed {removed_from_docket} file(s) from file_docket.json")
124
+ logger.info(f"Deleted {len(matching_ids)} chunks from {len(matching_files)} files matching '{filename_pattern}'")
125
+ logger.info(f"Removed {removed_from_docket} entries from file_docket")
126
+
127
+ return len(matching_ids)
128
+
129
+ except Exception as e:
130
+ logger.error(f"Failed to remove files from knowledge base for project: {project_name}")
131
+ logger.exception(e)
132
+ print(f"\nError: Failed to remove files. Check logs for details.")
133
+ raise
134
+
135
+
136
+ def list_matching_files(project_name: str, filename_pattern: str) -> List[str]:
137
+ """
138
+ List files that match the given filename pattern without deleting them.
139
+
140
+ Args:
141
+ project_name: Name of the project
142
+ filename_pattern: Full or partial filename to match (case-insensitive)
143
+
144
+ Returns:
145
+ List of matching filenames
146
+ """
147
+ try:
148
+ with Session() as db:
149
+ chroma_client = get_chromadb_client_for_project_name(db, project_name)
150
+
151
+ # Get all documents from the collection
152
+ collection = chroma_client._collection
153
+ results = collection.get()
154
+
155
+ if not results or not results.get('metadatas'):
156
+ return []
157
+
158
+ # Find matching files
159
+ matching_files: set = set()
160
+
161
+ for metadata in results['metadatas']:
162
+ if metadata:
163
+ file_name = metadata.get('file_name', '')
164
+ # Case-insensitive partial match
165
+ if filename_pattern.lower() in file_name.lower():
166
+ matching_files.add(file_name)
167
+
168
+ return sorted(list(matching_files))
169
+
170
+ except Exception as e:
171
+ logger.error(f"Failed to list matching files for project: {project_name}")
172
+ logger.exception(e)
173
+ return []
mfcli/utils/logger.py ADDED
@@ -0,0 +1,28 @@
1
+ import logging
2
+ import sys
3
+
4
+ from mfcli.utils.config import get_config
5
+
6
+
7
+ def setup_logging():
8
+ config = get_config()
9
+ formatter = logging.Formatter(
10
+ fmt="%(asctime)s [%(levelname)s] %(name)s:%(funcName)s:%(lineno)d: %(message)s",
11
+ datefmt="%Y-%m-%d %H:%M:%S"
12
+ )
13
+
14
+ handler = logging.StreamHandler(sys.stdout)
15
+ handler.setFormatter(formatter)
16
+
17
+ logger = logging.getLogger()
18
+ logger.setLevel(config.log_level)
19
+ logger.addHandler(handler)
20
+ logger.propagate = False
21
+
22
+ logging.getLogger("google_genai").setLevel(logging.ERROR)
23
+ logging.getLogger("httpx").setLevel(logging.ERROR)
24
+ logging.getLogger("docling").setLevel(logging.ERROR)
25
+
26
+
27
+ def get_logger(name=None):
28
+ return logging.getLogger(name or __name__)