fixos 2.2.29__tar.gz → 2.2.30__tar.gz
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.
- {fixos-2.2.29 → fixos-2.2.30}/CHANGELOG.md +9 -0
- {fixos-2.2.29 → fixos-2.2.30}/PKG-INFO +7 -5
- {fixos-2.2.29 → fixos-2.2.30}/README.md +5 -3
- {fixos-2.2.29 → fixos-2.2.30}/docker/validate-scenario.py +16 -5
- {fixos-2.2.29 → fixos-2.2.30}/docs/examples/advanced_usage.py +0 -3
- fixos-2.2.30/docs/examples/quickstart.py +5 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/__init__.py +2 -1
- {fixos-2.2.29 → fixos-2.2.30}/fixos/agent/__init__.py +2 -1
- {fixos-2.2.29 → fixos-2.2.30}/fixos/agent/autonomous_session.py +80 -48
- {fixos-2.2.29 → fixos-2.2.30}/fixos/agent/hitl.py +1 -1
- {fixos-2.2.29 → fixos-2.2.30}/fixos/agent/hitl_session.py +51 -26
- {fixos-2.2.29 → fixos-2.2.30}/fixos/agent/session_core.py +21 -10
- {fixos-2.2.29 → fixos-2.2.30}/fixos/agent/session_handlers.py +115 -94
- {fixos-2.2.29 → fixos-2.2.30}/fixos/agent/session_io.py +66 -29
- {fixos-2.2.29 → fixos-2.2.30}/fixos/anonymizer.py +19 -25
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/__init__.py +1 -0
- fixos-2.2.30/fixos/cli/_cleanup_flatpak.py +288 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/_cleanup_home.py +77 -28
- fixos-2.2.30/fixos/cli/_cleanup_snap.py +170 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/_cleanup_system.py +276 -117
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/_cleanup_utils.py +22 -21
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/ask_cmd.py +89 -68
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/cleanup_cmd.py +91 -34
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/config_cmd.py +58 -18
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/features_cmd.py +9 -4
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/fix_cmd.py +152 -58
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/history_cmd.py +6 -3
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/main.py +57 -29
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/orchestrate_cmd.py +55 -24
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/output_formatter.py +1 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/profile_cmd.py +1 -1
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/provider_cmd.py +30 -9
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/quickfix_cmd.py +13 -7
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/report_cmd.py +19 -5
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/rollback_cmd.py +11 -4
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/scan_cmd.py +119 -37
- fixos-2.2.30/fixos/cli/shared.py +105 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/token_cmd.py +25 -8
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli/watch_cmd.py +27 -9
- {fixos-2.2.29 → fixos-2.2.30}/fixos/cli.py +17 -5
- {fixos-2.2.29 → fixos-2.2.30}/fixos/config.py +19 -19
- {fixos-2.2.29 → fixos-2.2.30}/fixos/config_interactive.py +10 -6
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/__init__.py +1 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/_flatpak_analysis_mixin.py +50 -29
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/_flatpak_execution_mixin.py +46 -38
- fixos-2.2.30/fixos/diagnostics/_flatpak_recommendations_mixin.py +299 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/_storage_container_mixin.py +81 -60
- fixos-2.2.30/fixos/diagnostics/_storage_system_mixin.py +286 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/_storage_user_mixin.py +172 -125
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/checks/__init__.py +1 -1
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/checks/_shared.py +6 -2
- fixos-2.2.30/fixos/diagnostics/checks/audio.py +77 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/checks/file_analysis.py +33 -47
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/checks/hardware.py +21 -12
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/checks/packages.py +33 -50
- fixos-2.2.30/fixos/diagnostics/checks/resources.py +163 -0
- fixos-2.2.30/fixos/diagnostics/checks/security.py +142 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/checks/storage_optimization.py +31 -54
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/checks/system_core.py +43 -13
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/checks/thumbnails.py +33 -17
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/dev_project_analyzer.py +111 -74
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/disk_analyzer.py +232 -166
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/flatpak_analyzer.py +22 -17
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/service_cleanup.py +102 -73
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/service_details.py +43 -24
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/service_scanner.py +26 -9
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/storage_analyzer.py +66 -53
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/system_checks.py +1 -1
- {fixos-2.2.29 → fixos-2.2.30}/fixos/diagnostics/utils.py +1 -1
- {fixos-2.2.29 → fixos-2.2.30}/fixos/features/__init__.py +21 -8
- {fixos-2.2.29 → fixos-2.2.30}/fixos/features/auditor.py +2 -1
- {fixos-2.2.29 → fixos-2.2.30}/fixos/features/catalog.py +20 -11
- {fixos-2.2.29 → fixos-2.2.30}/fixos/features/installer.py +6 -8
- {fixos-2.2.29 → fixos-2.2.30}/fixos/features/profiles.py +15 -10
- {fixos-2.2.29 → fixos-2.2.30}/fixos/features/renderer.py +41 -23
- {fixos-2.2.29 → fixos-2.2.30}/fixos/interactive/cleanup_planner.py +160 -104
- {fixos-2.2.29 → fixos-2.2.30}/fixos/llm_shell.py +48 -24
- fixos-2.2.30/fixos/orchestrator/__init__.py +18 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/orchestrator/executor.py +28 -7
- {fixos-2.2.29 → fixos-2.2.30}/fixos/orchestrator/graph.py +10 -8
- {fixos-2.2.29 → fixos-2.2.30}/fixos/orchestrator/orchestrator.py +71 -35
- {fixos-2.2.29 → fixos-2.2.30}/fixos/orchestrator/rollback.py +49 -36
- {fixos-2.2.29 → fixos-2.2.30}/fixos/platform_utils.py +62 -13
- {fixos-2.2.29 → fixos-2.2.30}/fixos/plugins/__init__.py +1 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/plugins/base.py +4 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/plugins/builtin/audio.py +42 -32
- fixos-2.2.30/fixos/plugins/builtin/disk.py +133 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/plugins/builtin/hardware.py +41 -28
- {fixos-2.2.29 → fixos-2.2.30}/fixos/plugins/builtin/resources.py +42 -31
- {fixos-2.2.29 → fixos-2.2.30}/fixos/plugins/builtin/security.py +87 -59
- {fixos-2.2.29 → fixos-2.2.30}/fixos/plugins/builtin/thumbnails.py +49 -33
- {fixos-2.2.29 → fixos-2.2.30}/fixos/plugins/registry.py +20 -10
- {fixos-2.2.29 → fixos-2.2.30}/fixos/profiles/__init__.py +1 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/providers/__init__.py +1 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/providers/llm.py +24 -14
- {fixos-2.2.29 → fixos-2.2.30}/fixos/providers/llm_analyzer.py +93 -68
- {fixos-2.2.29 → fixos-2.2.30}/fixos/providers/schemas.py +12 -3
- fixos-2.2.30/fixos/system_checks.py +168 -0
- fixos-2.2.30/fixos/utils/__init__.py +16 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/utils/anonymizer.py +75 -34
- {fixos-2.2.29 → fixos-2.2.30}/fixos/utils/terminal.py +44 -23
- {fixos-2.2.29 → fixos-2.2.30}/fixos/utils/timeout.py +1 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/utils/web_search.py +68 -46
- {fixos-2.2.29 → fixos-2.2.30}/fixos/watch.py +9 -4
- {fixos-2.2.29 → fixos-2.2.30}/fixos.egg-info/PKG-INFO +7 -5
- {fixos-2.2.29 → fixos-2.2.30}/fixos.egg-info/top_level.txt +1 -0
- {fixos-2.2.29 → fixos-2.2.30}/pyproject.toml +2 -4
- {fixos-2.2.29 → fixos-2.2.30}/scripts/pyqual-calibrate.py +68 -77
- {fixos-2.2.29 → fixos-2.2.30}/setup.py +1 -1
- {fixos-2.2.29 → fixos-2.2.30}/tests/conftest.py +8 -4
- {fixos-2.2.29 → fixos-2.2.30}/tests/e2e/test_anonymization_layers.py +101 -77
- {fixos-2.2.29 → fixos-2.2.30}/tests/e2e/test_audio_broken.py +23 -11
- {fixos-2.2.29 → fixos-2.2.30}/tests/e2e/test_cli.py +127 -50
- {fixos-2.2.29 → fixos-2.2.30}/tests/e2e/test_diagnostics_integration.py +23 -4
- {fixos-2.2.29 → fixos-2.2.30}/tests/e2e/test_executor.py +27 -11
- {fixos-2.2.29 → fixos-2.2.30}/tests/e2e/test_multi_system.py +34 -34
- {fixos-2.2.29 → fixos-2.2.30}/tests/e2e/test_network_broken.py +18 -4
- {fixos-2.2.29 → fixos-2.2.30}/tests/e2e/test_thumbnails_broken.py +24 -7
- {fixos-2.2.29 → fixos-2.2.30}/tests/unit/test_anonymizer.py +1 -1
- {fixos-2.2.29 → fixos-2.2.30}/tests/unit/test_core.py +66 -8
- {fixos-2.2.29 → fixos-2.2.30}/tests/unit/test_orchestrator.py +191 -37
- {fixos-2.2.29 → fixos-2.2.30}/tests/unit/test_service_cleanup.py +6 -2
- {fixos-2.2.29 → fixos-2.2.30}/tests/unit/test_service_scanner.py +6 -2
- fixos-2.2.29/docs/examples/quickstart.py +0 -14
- fixos-2.2.29/fixos/cli/_cleanup_flatpak.py +0 -229
- fixos-2.2.29/fixos/cli/_cleanup_snap.py +0 -116
- fixos-2.2.29/fixos/cli/shared.py +0 -62
- fixos-2.2.29/fixos/diagnostics/_flatpak_recommendations_mixin.py +0 -259
- fixos-2.2.29/fixos/diagnostics/_storage_system_mixin.py +0 -259
- fixos-2.2.29/fixos/diagnostics/checks/audio.py +0 -61
- fixos-2.2.29/fixos/diagnostics/checks/resources.py +0 -99
- fixos-2.2.29/fixos/diagnostics/checks/security.py +0 -92
- fixos-2.2.29/fixos/orchestrator/__init__.py +0 -9
- fixos-2.2.29/fixos/plugins/builtin/disk.py +0 -113
- fixos-2.2.29/fixos/system_checks.py +0 -156
- fixos-2.2.29/fixos/utils/__init__.py +0 -3
- {fixos-2.2.29 → fixos-2.2.30}/.env.example +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/LICENSE +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/MANIFEST.in +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/docker/README.md +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/docker/TEST_RESULTS.md +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/docker/TEST_RESULTS_V2.md +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/docker/alpine/Dockerfile +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/docker/arch/Dockerfile +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/docker/base/Dockerfile +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/docker/broken-audio/Dockerfile +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/docker/broken-full/Dockerfile +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/docker/broken-network/Dockerfile +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/docker/broken-thumbnails/Dockerfile +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/docker/debian/Dockerfile +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/docker/docker-compose.multi-system.yml +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/docker/docker-compose.yml +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/docker/fedora/Dockerfile +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/docker/test-multi-system.sh +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/docker/test-scenarios.sh +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/docker/ubuntu/Dockerfile +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/agent/autonomous.py +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/constants.py +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/fixes/__init__.py +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/interactive/__init__.py +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos/plugins/builtin/__init__.py +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos.egg-info/SOURCES.txt +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos.egg-info/dependency_links.txt +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos.egg-info/entry_points.txt +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/fixos.egg-info/requires.txt +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/pytest.ini +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/requirements-dev.txt +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/requirements.txt +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/setup.cfg +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/tests/__init__.py +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/tests/e2e/__init__.py +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/tests/test_fixos.py +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/tests/unit/__init__.py +0 -0
- {fixos-2.2.29 → fixos-2.2.30}/tests/unit/test_executor.py +0 -0
|
@@ -150,6 +150,15 @@ fix(goal): code analysis engine
|
|
|
150
150
|
- **refactor(cli):** Usunięto zduplikowany kod ujednolicając funkcje analizy dysku do wspólnego helpera `_run_disk_analysis`.
|
|
151
151
|
- **refactor(ui):** Usunięto ikony Unicode z CLI i sformatowano wyjście `stderr` oraz standardowego logowania na czysty kod Markdown dla poprawy czytelności w oknach terminalowych.
|
|
152
152
|
|
|
153
|
+
## [2.2.30] - 2026-06-16
|
|
154
|
+
|
|
155
|
+
### Docs
|
|
156
|
+
- Update README.md
|
|
157
|
+
|
|
158
|
+
### Other
|
|
159
|
+
- Update nlp2uri.yaml
|
|
160
|
+
- Update uv.lock
|
|
161
|
+
|
|
153
162
|
## [2.2.29] - 2026-05-08
|
|
154
163
|
|
|
155
164
|
### Docs
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fixos
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.30
|
|
4
4
|
Summary: AI-powered Linux/Windows diagnostics and repair – audio, hardware, system issues
|
|
5
5
|
Home-page: https://github.com/wronai/fixos
|
|
6
6
|
Author: fixos contributors
|
|
7
7
|
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
8
|
-
License: Apache-2.0
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
9
|
Project-URL: Homepage, https://github.com/wronai/fixos
|
|
10
10
|
Project-URL: Bug Tracker, https://github.com/wronai/fixos/issues
|
|
11
11
|
Keywords: linux,windows,diagnostics,ai,llm,audio,system-repair,cross-platform
|
|
@@ -63,11 +63,13 @@ AI-powered OS Diagnostics
|
|
|
63
63
|
|
|
64
64
|
## AI Cost Tracking
|
|
65
65
|
|
|
66
|
-
   
|
|
67
|
+
  
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
- 🤖 **LLM usage:** $3.6649 (131 commits)
|
|
70
|
+
- 👤 **Human dev:** ~$3047 (30.5h @ $100/h, 30min dedup)
|
|
69
71
|
|
|
70
|
-
Generated on 2026-
|
|
72
|
+
Generated on 2026-06-16 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
71
73
|
|
|
72
74
|
---
|
|
73
75
|
|
|
@@ -19,11 +19,13 @@ AI-powered OS Diagnostics
|
|
|
19
19
|
|
|
20
20
|
## AI Cost Tracking
|
|
21
21
|
|
|
22
|
-
   
|
|
23
|
+
  
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
- 🤖 **LLM usage:** $3.6649 (131 commits)
|
|
26
|
+
- 👤 **Human dev:** ~$3047 (30.5h @ $100/h, 30min dedup)
|
|
25
27
|
|
|
26
|
-
Generated on 2026-
|
|
28
|
+
Generated on 2026-06-16 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
27
29
|
|
|
28
30
|
---
|
|
29
31
|
|
|
@@ -85,7 +85,9 @@ def validate(data: dict, scenario: str) -> list[str]:
|
|
|
85
85
|
"""Validate data against scenario expectations. Returns list of failures."""
|
|
86
86
|
expectations = SCENARIOS.get(scenario)
|
|
87
87
|
if expectations is None:
|
|
88
|
-
return [
|
|
88
|
+
return [
|
|
89
|
+
f"Nieznany scenariusz: '{scenario}'. Dostępne: {', '.join(SCENARIOS.keys())}"
|
|
90
|
+
]
|
|
89
91
|
|
|
90
92
|
failures = []
|
|
91
93
|
for exp in expectations:
|
|
@@ -101,14 +103,20 @@ def validate(data: dict, scenario: str) -> list[str]:
|
|
|
101
103
|
|
|
102
104
|
if "equals" in exp:
|
|
103
105
|
if value_str.strip() != str(exp["equals"]).strip():
|
|
104
|
-
failures.append(
|
|
106
|
+
failures.append(
|
|
107
|
+
f" ✗ {desc}: oczekiwano '{exp['equals']}', otrzymano '{value_str[:80]}'"
|
|
108
|
+
)
|
|
105
109
|
|
|
106
110
|
if "contains" in exp:
|
|
107
111
|
if exp["contains"].lower() not in value_str.lower():
|
|
108
|
-
failures.append(
|
|
112
|
+
failures.append(
|
|
113
|
+
f" ✗ {desc}: brak '{exp['contains']}' w '{value_str[:80]}'"
|
|
114
|
+
)
|
|
109
115
|
|
|
110
116
|
if "contains_any" in exp:
|
|
111
|
-
if not any(
|
|
117
|
+
if not any(
|
|
118
|
+
term.lower() in value_str.lower() for term in exp["contains_any"]
|
|
119
|
+
):
|
|
112
120
|
failures.append(
|
|
113
121
|
f" ✗ {desc}: brak żadnego z {exp['contains_any']} w '{value_str[:80]}'"
|
|
114
122
|
)
|
|
@@ -150,7 +158,10 @@ def main() -> None:
|
|
|
150
158
|
sys.exit(1)
|
|
151
159
|
else:
|
|
152
160
|
expectations_count = len(SCENARIOS[scenario])
|
|
153
|
-
print(
|
|
161
|
+
print(
|
|
162
|
+
f"✓ Scenariusz '{scenario}' — {expectations_count}/{expectations_count} OK",
|
|
163
|
+
file=sys.stderr,
|
|
164
|
+
)
|
|
154
165
|
sys.exit(0)
|
|
155
166
|
|
|
156
167
|
|
|
@@ -21,9 +21,10 @@ if TYPE_CHECKING:
|
|
|
21
21
|
|
|
22
22
|
def get_remaining_time(session: "HITLSession | AutonomousSession") -> int:
|
|
23
23
|
"""Calculate remaining session time in seconds."""
|
|
24
|
-
start_time = getattr(session,
|
|
24
|
+
start_time = getattr(session, "start_ts", None) or getattr(session, "start_time")
|
|
25
25
|
return session.config.session_timeout - int(time.time() - start_time)
|
|
26
26
|
|
|
27
|
+
|
|
27
28
|
__all__ = [
|
|
28
29
|
"run_hitl_session",
|
|
29
30
|
"HITLSession",
|
|
@@ -18,7 +18,6 @@ from ..utils.timeout import SessionTimeout
|
|
|
18
18
|
from ..constants import (
|
|
19
19
|
UI_BORDER_WIDTH,
|
|
20
20
|
MAX_OUTPUT_PREVIEW_LENGTH,
|
|
21
|
-
MAX_ANON_PREVIEW_LENGTH,
|
|
22
21
|
DEFAULT_COMMAND_TIMEOUT,
|
|
23
22
|
DEFAULT_TOKEN_LIMIT,
|
|
24
23
|
MAX_COMMAND_LENGTH,
|
|
@@ -26,7 +25,6 @@ from ..constants import (
|
|
|
26
25
|
)
|
|
27
26
|
|
|
28
27
|
|
|
29
|
-
|
|
30
28
|
# Commands NEVER executed automatically
|
|
31
29
|
FORBIDDEN_COMMANDS = [
|
|
32
30
|
r"rm\s+-rf\s+/",
|
|
@@ -42,8 +40,16 @@ FORBIDDEN_COMMANDS = [
|
|
|
42
40
|
]
|
|
43
41
|
|
|
44
42
|
SUDO_PREFIXES = [
|
|
45
|
-
"dnf",
|
|
46
|
-
"
|
|
43
|
+
"dnf",
|
|
44
|
+
"rpm",
|
|
45
|
+
"systemctl",
|
|
46
|
+
"firewall-cmd",
|
|
47
|
+
"setenforce",
|
|
48
|
+
"modprobe",
|
|
49
|
+
"rmmod",
|
|
50
|
+
"alsactl",
|
|
51
|
+
"grub2-",
|
|
52
|
+
"update-grub",
|
|
47
53
|
]
|
|
48
54
|
|
|
49
55
|
SYSTEM_PROMPT_AUTONOMOUS = """Jesteś autonomicznym agentem diagnostyki Linux, Windows, macOS.
|
|
@@ -130,8 +136,10 @@ class AutonomousSession:
|
|
|
130
136
|
|
|
131
137
|
def _setup_timeout(self) -> None:
|
|
132
138
|
"""Setup session timeout handler."""
|
|
139
|
+
|
|
133
140
|
def _timeout_handler(signum, frame) -> None:
|
|
134
141
|
raise SessionTimeout()
|
|
142
|
+
|
|
135
143
|
signal.signal(signal.SIGALRM, _timeout_handler)
|
|
136
144
|
signal.alarm(self.config.session_timeout)
|
|
137
145
|
|
|
@@ -149,7 +157,9 @@ class AutonomousSession:
|
|
|
149
157
|
print(f" Timeout sesji: {self.config.session_timeout}s")
|
|
150
158
|
print(f" Model: {self.config.model}")
|
|
151
159
|
|
|
152
|
-
confirm = input(
|
|
160
|
+
confirm = input(
|
|
161
|
+
"\n Czy na pewno chcesz uruchomić tryb autonomiczny? (yes/N): "
|
|
162
|
+
).strip()
|
|
153
163
|
if confirm.lower() not in ("yes", "tak"):
|
|
154
164
|
print(" Anulowano. Użyj --mode hitl dla trybu z potwierdzeniem.")
|
|
155
165
|
return False
|
|
@@ -175,6 +185,7 @@ class AutonomousSession:
|
|
|
175
185
|
def _get_remaining_time(self) -> int:
|
|
176
186
|
"""Get remaining session time in seconds."""
|
|
177
187
|
from . import get_remaining_time
|
|
188
|
+
|
|
178
189
|
return get_remaining_time(self)
|
|
179
190
|
|
|
180
191
|
def _check_timeout(self) -> None:
|
|
@@ -185,7 +196,9 @@ class AutonomousSession:
|
|
|
185
196
|
def _query_llm(self) -> Optional[str]:
|
|
186
197
|
"""Query LLM and return reply."""
|
|
187
198
|
try:
|
|
188
|
-
return self.llm.chat(
|
|
199
|
+
return self.llm.chat(
|
|
200
|
+
self.messages, max_tokens=DEFAULT_TOKEN_LIMIT, temperature=0.1
|
|
201
|
+
)
|
|
189
202
|
except LLMError as e:
|
|
190
203
|
print(f" ❌ LLM błąd: {e}")
|
|
191
204
|
return None
|
|
@@ -195,10 +208,9 @@ class AutonomousSession:
|
|
|
195
208
|
if self.config.enable_web_search and self.search_count < self.MAX_SEARCHES:
|
|
196
209
|
results = search_all("fedora repair diagnostics", self.config.serpapi_key)
|
|
197
210
|
if results:
|
|
198
|
-
self.messages.append(
|
|
199
|
-
"role": "user",
|
|
200
|
-
|
|
201
|
-
})
|
|
211
|
+
self.messages.append(
|
|
212
|
+
{"role": "user", "content": format_results_for_llm(results)}
|
|
213
|
+
)
|
|
202
214
|
self.search_count += 1
|
|
203
215
|
return True
|
|
204
216
|
return False
|
|
@@ -206,6 +218,7 @@ class AutonomousSession:
|
|
|
206
218
|
def _parse_action(self, reply: str) -> Optional[Dict[str, Any]]:
|
|
207
219
|
"""Parse JSON action from LLM reply."""
|
|
208
220
|
import json
|
|
221
|
+
|
|
209
222
|
patterns = [
|
|
210
223
|
r"```(?:json)?\s*(\{.*?\})\s*```",
|
|
211
224
|
r"(\{[^{}]*\"action\"[^{}]*\})",
|
|
@@ -242,7 +255,11 @@ class AutonomousSession:
|
|
|
242
255
|
"""Execute command and return (success, output)."""
|
|
243
256
|
try:
|
|
244
257
|
proc = subprocess.run(
|
|
245
|
-
cmd,
|
|
258
|
+
cmd,
|
|
259
|
+
shell=True,
|
|
260
|
+
capture_output=True,
|
|
261
|
+
text=True,
|
|
262
|
+
timeout=DEFAULT_COMMAND_TIMEOUT,
|
|
246
263
|
)
|
|
247
264
|
out = proc.stdout.strip() or proc.stderr.strip() or "(brak outputu)"
|
|
248
265
|
return proc.returncode == 0, out[:MAX_OUTPUT_PREVIEW_LENGTH]
|
|
@@ -260,22 +277,28 @@ class AutonomousSession:
|
|
|
260
277
|
self.report.searches_done.append(query)
|
|
261
278
|
if results:
|
|
262
279
|
web_ctx = format_results_for_llm(results)
|
|
263
|
-
self.messages.append(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
280
|
+
self.messages.append(
|
|
281
|
+
{
|
|
282
|
+
"role": "user",
|
|
283
|
+
"content": f"Wyniki dla '{query}':\n{web_ctx}\nKontynuuj naprawę.",
|
|
284
|
+
}
|
|
285
|
+
)
|
|
267
286
|
else:
|
|
268
|
-
self.messages.append(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
287
|
+
self.messages.append(
|
|
288
|
+
{
|
|
289
|
+
"role": "user",
|
|
290
|
+
"content": f"Brak wyników dla '{query}'. Co innego możemy zrobić?",
|
|
291
|
+
}
|
|
292
|
+
)
|
|
272
293
|
return True
|
|
273
294
|
else:
|
|
274
295
|
print(" ⚠️ Limit wyszukiwań osiągnięty.")
|
|
275
|
-
self.messages.append(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
296
|
+
self.messages.append(
|
|
297
|
+
{
|
|
298
|
+
"role": "user",
|
|
299
|
+
"content": "Brak więcej wyszukiwań. Co możemy zrobić bez zewnętrznych źródeł?",
|
|
300
|
+
}
|
|
301
|
+
)
|
|
279
302
|
return False
|
|
280
303
|
|
|
281
304
|
def _handle_exec(self, action_data: Dict[str, Any]) -> bool:
|
|
@@ -284,17 +307,21 @@ class AutonomousSession:
|
|
|
284
307
|
reason = action_data.get("reason", "")
|
|
285
308
|
|
|
286
309
|
if not cmd_raw:
|
|
287
|
-
self.messages.append(
|
|
310
|
+
self.messages.append(
|
|
311
|
+
{"role": "user", "content": "Brak komendy. Podaj konkretną komendę."}
|
|
312
|
+
)
|
|
288
313
|
return False
|
|
289
314
|
|
|
290
315
|
# Security check
|
|
291
316
|
danger = self._is_forbidden(cmd_raw)
|
|
292
317
|
if danger:
|
|
293
318
|
print(f" 🚫 ZABLOKOWANO: {danger}")
|
|
294
|
-
self.messages.append(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
319
|
+
self.messages.append(
|
|
320
|
+
{
|
|
321
|
+
"role": "user",
|
|
322
|
+
"content": f"Komenda `{cmd_raw}` jest zabroniona: {danger}. Zaproponuj bezpieczniejszą alternatywę.",
|
|
323
|
+
}
|
|
324
|
+
)
|
|
298
325
|
return False
|
|
299
326
|
|
|
300
327
|
cmd = deanonymize(cmd_raw)
|
|
@@ -313,25 +340,26 @@ class AutonomousSession:
|
|
|
313
340
|
anon_out, _ = anonymize(out)
|
|
314
341
|
anon_cmd, _ = anonymize(cmd)
|
|
315
342
|
|
|
316
|
-
self.messages.append(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
343
|
+
self.messages.append(
|
|
344
|
+
{
|
|
345
|
+
"role": "user",
|
|
346
|
+
"content": (
|
|
347
|
+
f"Wykonano: `{anon_cmd}`\n"
|
|
348
|
+
f"Sukces: {ok}\n"
|
|
349
|
+
f"Output: {anon_out[:MAX_SEARCH_QUERY_LENGTH]}\n"
|
|
350
|
+
f"Zweryfikuj wynik i zaproponuj następną akcję."
|
|
351
|
+
),
|
|
352
|
+
}
|
|
353
|
+
)
|
|
325
354
|
return True
|
|
326
355
|
|
|
327
356
|
def _handle_skip(self, action_data: Dict[str, Any]) -> None:
|
|
328
357
|
"""Handle SKIP action."""
|
|
329
358
|
reason = action_data.get("reason", "")
|
|
330
359
|
print(f" ⏭️ Pomijam: {reason}")
|
|
331
|
-
self.messages.append(
|
|
332
|
-
"role": "user",
|
|
333
|
-
|
|
334
|
-
})
|
|
360
|
+
self.messages.append(
|
|
361
|
+
{"role": "user", "content": f"Pominięto: {reason}. Co dalej?"}
|
|
362
|
+
)
|
|
335
363
|
self.fix_count += 1
|
|
336
364
|
|
|
337
365
|
def _handle_done(self) -> None:
|
|
@@ -345,7 +373,9 @@ class AutonomousSession:
|
|
|
345
373
|
"""
|
|
346
374
|
self._check_timeout()
|
|
347
375
|
remaining = self._get_remaining_time()
|
|
348
|
-
print(
|
|
376
|
+
print(
|
|
377
|
+
f" ⟳ Tura {self.fix_count + 1}/{self.max_fixes} | ⏰ {remaining}s pozostało"
|
|
378
|
+
)
|
|
349
379
|
|
|
350
380
|
# Query LLM
|
|
351
381
|
reply = self._query_llm()
|
|
@@ -360,10 +390,12 @@ class AutonomousSession:
|
|
|
360
390
|
action_data = self._parse_action(reply)
|
|
361
391
|
if not action_data:
|
|
362
392
|
print(" ⚠️ Nieprawidłowy format JSON, kontynuuję...")
|
|
363
|
-
self.messages.append(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
393
|
+
self.messages.append(
|
|
394
|
+
{
|
|
395
|
+
"role": "user",
|
|
396
|
+
"content": "Odpowiedz TYLKO w formacie JSON jak w instrukcji.",
|
|
397
|
+
}
|
|
398
|
+
)
|
|
367
399
|
return True
|
|
368
400
|
|
|
369
401
|
action = action_data.get("action", "SKIP")
|
|
@@ -407,9 +439,9 @@ class AutonomousSession:
|
|
|
407
439
|
if not should_continue:
|
|
408
440
|
break
|
|
409
441
|
except SessionTimeout:
|
|
410
|
-
print(
|
|
442
|
+
print("\n ⏰ Timeout sesji.")
|
|
411
443
|
except KeyboardInterrupt:
|
|
412
|
-
print(
|
|
444
|
+
print("\n\n ⛔ Przerwano przez użytkownika (Ctrl+C).")
|
|
413
445
|
finally:
|
|
414
446
|
self._clear_timeout()
|
|
415
447
|
|
|
@@ -24,7 +24,7 @@ __all__ = [
|
|
|
24
24
|
def run_hitl_session(diagnostics: dict, config, show_data: bool = True) -> None:
|
|
25
25
|
"""
|
|
26
26
|
Run interactive HITL session with full transparency.
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
This is a backward-compatible wrapper around HITLSession.
|
|
29
29
|
For new code, use HITLSession directly.
|
|
30
30
|
"""
|
|
@@ -10,15 +10,11 @@ from ..providers.llm import LLMClient, LLMError
|
|
|
10
10
|
from ..utils.anonymizer import anonymize, deanonymize, display_anonymized_preview
|
|
11
11
|
from ..utils.web_search import search_all, format_results_for_llm
|
|
12
12
|
from ..config import FixOsConfig
|
|
13
|
-
from ..constants import (
|
|
14
|
-
HITL_TIMEOUT_BUFFER,
|
|
15
|
-
AUTONOMOU_TIMEOUT_BUFFER,
|
|
16
|
-
CLEANUP_TIMEOUT_ESTIMATE,
|
|
17
|
-
MAX_COMMAND_LENGTH,
|
|
18
|
-
)
|
|
19
13
|
from ..platform_utils import (
|
|
20
|
-
setup_signal_timeout,
|
|
21
|
-
|
|
14
|
+
setup_signal_timeout,
|
|
15
|
+
cancel_signal_timeout,
|
|
16
|
+
get_os_info,
|
|
17
|
+
get_package_manager,
|
|
22
18
|
)
|
|
23
19
|
from ..utils.timeout import SessionTimeout
|
|
24
20
|
|
|
@@ -54,8 +50,10 @@ class HITLSession:
|
|
|
54
50
|
def _setup_timeout(self) -> None:
|
|
55
51
|
"""Setup session timeout handler."""
|
|
56
52
|
from . import session_io
|
|
53
|
+
|
|
57
54
|
def _timeout(signum, frame) -> None:
|
|
58
55
|
raise SessionTimeout()
|
|
56
|
+
|
|
59
57
|
# Store reference in session_io for reinstatement during user input
|
|
60
58
|
session_io._setup_timeout_ref(self, self.config.session_timeout, _timeout)
|
|
61
59
|
setup_signal_timeout(self.config.session_timeout, _timeout)
|
|
@@ -67,6 +65,7 @@ class HITLSession:
|
|
|
67
65
|
def remaining(self) -> int:
|
|
68
66
|
"""Get remaining session time in seconds."""
|
|
69
67
|
from . import get_remaining_time
|
|
68
|
+
|
|
70
69
|
return get_remaining_time(self)
|
|
71
70
|
|
|
72
71
|
def _initialize_messages(self) -> bool:
|
|
@@ -96,17 +95,24 @@ class HITLSession:
|
|
|
96
95
|
def _print_header(self) -> None:
|
|
97
96
|
"""Print session header with system info."""
|
|
98
97
|
io.print_session_header(
|
|
99
|
-
self.os_info,
|
|
100
|
-
self.
|
|
101
|
-
self.
|
|
98
|
+
self.os_info,
|
|
99
|
+
self.pkg_manager,
|
|
100
|
+
self.config.model,
|
|
101
|
+
self.config.session_timeout,
|
|
102
|
+
self.remaining,
|
|
102
103
|
)
|
|
103
104
|
|
|
104
105
|
def _handle_llm_error(self) -> bool:
|
|
105
106
|
"""Handle LLM error - try web search if enabled."""
|
|
106
|
-
if
|
|
107
|
+
if (
|
|
108
|
+
self.config.enable_web_search
|
|
109
|
+
and self.web_search_count < self.MAX_WEB_SEARCHES
|
|
110
|
+
):
|
|
107
111
|
self.web_search_count += 1
|
|
108
112
|
io.print_searching()
|
|
109
|
-
results = search_all(
|
|
113
|
+
results = search_all(
|
|
114
|
+
"linux system diagnostics repair", self.config.serpapi_key
|
|
115
|
+
)
|
|
110
116
|
if results:
|
|
111
117
|
io.console.print(format_results_for_llm(results))
|
|
112
118
|
return True
|
|
@@ -114,11 +120,21 @@ class HITLSession:
|
|
|
114
120
|
|
|
115
121
|
def _check_low_confidence(self, reply: str) -> bool:
|
|
116
122
|
"""Check if LLM is uncertain and perform web search if enabled."""
|
|
117
|
-
low_conf = any(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
low_conf = any(
|
|
124
|
+
p in reply.lower()
|
|
125
|
+
for p in [
|
|
126
|
+
"nie wiem",
|
|
127
|
+
"nie jestem pewien",
|
|
128
|
+
"i don't know",
|
|
129
|
+
"not sure",
|
|
130
|
+
"cannot determine",
|
|
131
|
+
]
|
|
132
|
+
)
|
|
133
|
+
if (
|
|
134
|
+
low_conf
|
|
135
|
+
and self.config.enable_web_search
|
|
136
|
+
and self.web_search_count < self.MAX_WEB_SEARCHES
|
|
137
|
+
):
|
|
122
138
|
if io.ask_low_confidence_search():
|
|
123
139
|
self.web_search_count += 1
|
|
124
140
|
topic = extract_search_topic(reply)
|
|
@@ -126,8 +142,12 @@ class HITLSession:
|
|
|
126
142
|
if results:
|
|
127
143
|
web_ctx = format_results_for_llm(results)
|
|
128
144
|
io.console.print(web_ctx)
|
|
129
|
-
self.messages.append(
|
|
130
|
-
|
|
145
|
+
self.messages.append(
|
|
146
|
+
{
|
|
147
|
+
"role": "user",
|
|
148
|
+
"content": f"External sources:\n{web_ctx}\nUpdate analysis.",
|
|
149
|
+
}
|
|
150
|
+
)
|
|
131
151
|
return True
|
|
132
152
|
return False
|
|
133
153
|
|
|
@@ -164,14 +184,17 @@ class HITLSession:
|
|
|
164
184
|
|
|
165
185
|
# Handle all command types via handlers module
|
|
166
186
|
should_continue, was_handled = handlers.parse_user_input(
|
|
167
|
-
user_in,
|
|
168
|
-
self.
|
|
187
|
+
user_in,
|
|
188
|
+
self.last_fixes,
|
|
189
|
+
self.messages,
|
|
190
|
+
self.executed,
|
|
191
|
+
self.config.serpapi_key,
|
|
169
192
|
)
|
|
170
|
-
|
|
193
|
+
|
|
171
194
|
if not was_handled:
|
|
172
195
|
# Free text → send to LLM
|
|
173
196
|
self.messages.append({"role": "user", "content": user_in})
|
|
174
|
-
|
|
197
|
+
|
|
175
198
|
return should_continue
|
|
176
199
|
|
|
177
200
|
def run(self) -> None:
|
|
@@ -195,7 +218,9 @@ class HITLSession:
|
|
|
195
218
|
def _print_summary(self) -> None:
|
|
196
219
|
"""Print session summary."""
|
|
197
220
|
elapsed = int(time.time() - self.start_ts)
|
|
198
|
-
io.print_session_summary(
|
|
221
|
+
io.print_session_summary(
|
|
222
|
+
len(self.messages) - 2, elapsed, self.llm.total_tokens, self.executed
|
|
223
|
+
)
|
|
199
224
|
|
|
200
225
|
|
|
201
226
|
def run_hitl_session(
|
|
@@ -215,7 +240,7 @@ def run_hitl_session(
|
|
|
215
240
|
# Backward compatibility exports
|
|
216
241
|
__all__ = [
|
|
217
242
|
"CmdResult",
|
|
218
|
-
"HITLSession",
|
|
243
|
+
"HITLSession",
|
|
219
244
|
"run_hitl_session",
|
|
220
245
|
"SYSTEM_PROMPT",
|
|
221
246
|
]
|
|
@@ -16,6 +16,7 @@ from ..constants import (
|
|
|
16
16
|
@dataclass
|
|
17
17
|
class CmdResult:
|
|
18
18
|
"""Result of executed command."""
|
|
19
|
+
|
|
19
20
|
cmd: str
|
|
20
21
|
comment: str
|
|
21
22
|
ok: bool
|
|
@@ -92,8 +93,8 @@ IMPORTANT: Adapt commands to the detected OS (Linux/Windows/macOS).
|
|
|
92
93
|
def _is_diagnostic_only_command(cmd: str) -> bool:
|
|
93
94
|
"""Return True if command is read-only and not a repair action."""
|
|
94
95
|
# Split by common shell delimiters to check each part
|
|
95
|
-
parts = re.split(r
|
|
96
|
-
|
|
96
|
+
parts = re.split(r" && | \|\| |; ", cmd)
|
|
97
|
+
|
|
97
98
|
# If any part of a compound command looks like a repair, the whole thing is actionable
|
|
98
99
|
for part in parts:
|
|
99
100
|
if not _is_part_diagnostic_only(part):
|
|
@@ -109,7 +110,11 @@ def _is_part_diagnostic_only(part: str) -> bool:
|
|
|
109
110
|
|
|
110
111
|
# Special case: diagnostic tools used for cleanup/repair
|
|
111
112
|
if normalized.startswith("journalctl"):
|
|
112
|
-
if
|
|
113
|
+
if (
|
|
114
|
+
"--vacuum-" in normalized
|
|
115
|
+
or "--flush" in normalized
|
|
116
|
+
or "--rotate" in normalized
|
|
117
|
+
):
|
|
113
118
|
return False
|
|
114
119
|
|
|
115
120
|
diagnostic_prefixes = (
|
|
@@ -147,7 +152,8 @@ def _pattern_strict_bold(reply: str) -> List[Tuple[str, str]]:
|
|
|
147
152
|
fixes: List[Tuple[str, str]] = []
|
|
148
153
|
for m in re.finditer(
|
|
149
154
|
r"\*\*Komenda:\*\*\s*`([^`]+)`(?:[^\n]*?\*\*Co robi:\*\*\s*(.+?))?(?=\n|$)",
|
|
150
|
-
reply,
|
|
155
|
+
reply,
|
|
156
|
+
re.IGNORECASE,
|
|
151
157
|
):
|
|
152
158
|
cmd = m.group(1).strip()
|
|
153
159
|
if cmd:
|
|
@@ -160,11 +166,12 @@ def _pattern_backticks(reply: str) -> List[Tuple[str, str]]:
|
|
|
160
166
|
fixes: List[Tuple[str, str]] = []
|
|
161
167
|
for m in re.finditer(
|
|
162
168
|
r"\*{0,2}Komenda:\*{0,2}\s*`([^`]+)`",
|
|
163
|
-
reply,
|
|
169
|
+
reply,
|
|
170
|
+
re.IGNORECASE,
|
|
164
171
|
):
|
|
165
172
|
cmd = m.group(1).strip()
|
|
166
173
|
if cmd:
|
|
167
|
-
fixes.append((cmd, _extract_co_robi(reply[m.end():])))
|
|
174
|
+
fixes.append((cmd, _extract_co_robi(reply[m.end() :])))
|
|
168
175
|
return fixes
|
|
169
176
|
|
|
170
177
|
|
|
@@ -175,11 +182,12 @@ def _pattern_no_backticks(reply: str) -> List[Tuple[str, str]]:
|
|
|
175
182
|
r"\*{0,2}Komenda:\*{0,2}\s*"
|
|
176
183
|
r"(.+?)"
|
|
177
184
|
r"(?=\n\s*\*{0,2}Co robi:|\n[🔴🟡🟢]|\n━|\n─|\n\[[\dA-Z]|\Z)",
|
|
178
|
-
reply,
|
|
185
|
+
reply,
|
|
186
|
+
re.IGNORECASE | re.DOTALL,
|
|
179
187
|
):
|
|
180
188
|
cmd = re.sub(r"\s*\n\s*", " ", m.group(1)).strip()
|
|
181
189
|
if cmd:
|
|
182
|
-
fixes.append((cmd, _extract_co_robi(reply[m.end():])))
|
|
190
|
+
fixes.append((cmd, _extract_co_robi(reply[m.end() :])))
|
|
183
191
|
return fixes
|
|
184
192
|
|
|
185
193
|
|
|
@@ -199,7 +207,9 @@ def _pattern_fallbacks(reply: str) -> List[Tuple[str, str]]:
|
|
|
199
207
|
|
|
200
208
|
def _deduplicate(fixes: List[Tuple[str, str]]) -> List[Tuple[str, str]]:
|
|
201
209
|
"""Remove diagnostic-only commands and deduplicate."""
|
|
202
|
-
filtered = [
|
|
210
|
+
filtered = [
|
|
211
|
+
(cmd, comment) for cmd, comment in fixes if not _is_diagnostic_only_command(cmd)
|
|
212
|
+
]
|
|
203
213
|
seen: set[str] = set()
|
|
204
214
|
unique: List[Tuple[str, str]] = []
|
|
205
215
|
for cmd, comment in filtered:
|
|
@@ -226,7 +236,8 @@ def extract_search_topic(llm_reply: str) -> str:
|
|
|
226
236
|
r"\b(sof-firmware|pipewire|alsa|thumbnails?|nautilus|"
|
|
227
237
|
r"dnf|apt|systemctl|journalctl|codec|driver|nvidia|amd|"
|
|
228
238
|
r"snd_hda|intel_sst|avs|wireplumber|pulseaudio|bluetooth|wifi)\b",
|
|
229
|
-
llm_reply,
|
|
239
|
+
llm_reply,
|
|
240
|
+
re.IGNORECASE,
|
|
230
241
|
)
|
|
231
242
|
if tech_terms:
|
|
232
243
|
return " ".join(dict.fromkeys(tech_terms[:MAX_TECH_TERMS]))
|