labpilot-ai 0.1.2__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.
- labpilot_ai/__init__.py +1 -0
- labpilot_ai/__main__.py +4 -0
- labpilot_ai/ai/__init__.py +1 -0
- labpilot_ai/ai/co_sequence_prompt.py +86 -0
- labpilot_ai/ai/command_schema.py +30 -0
- labpilot_ai/ai/error_advisor.py +39 -0
- labpilot_ai/ai/json_parser.py +14 -0
- labpilot_ai/ai/llm_client.py +127 -0
- labpilot_ai/ai/prompt_builder.py +95 -0
- labpilot_ai/ai/protocol_designer.py +23 -0
- labpilot_ai/ai/protocol_importer.py +57 -0
- labpilot_ai/analysis/__init__.py +1 -0
- labpilot_ai/analysis/fit_models.py +186 -0
- labpilot_ai/analysis/plotting.py +89 -0
- labpilot_ai/analysis/report_generator.py +61 -0
- labpilot_ai/app/__init__.py +1 -0
- labpilot_ai/app/error_center.py +167 -0
- labpilot_ai/app/main_window.py +3885 -0
- labpilot_ai/app/theme.py +105 -0
- labpilot_ai/blacs_ctrl/__init__.py +1 -0
- labpilot_ai/blacs_ctrl/manual_bridge_server.py +47 -0
- labpilot_ai/blacs_ctrl/manual_client.py +31 -0
- labpilot_ai/bootstrap_labscript.py +21 -0
- labpilot_ai/co_sequence/__init__.py +17 -0
- labpilot_ai/co_sequence/log_store.py +90 -0
- labpilot_ai/co_sequence/patch_validator.py +340 -0
- labpilot_ai/config/__init__.py +1 -0
- labpilot_ai/config/registry_editor.py +315 -0
- labpilot_ai/config/settings_manager.py +100 -0
- labpilot_ai/directory/__init__.py +5 -0
- labpilot_ai/directory/path_registry.py +186 -0
- labpilot_ai/experiment_log/__init__.py +5 -0
- labpilot_ai/experiment_log/generator.py +145 -0
- labpilot_ai/knowledge/__init__.py +1 -0
- labpilot_ai/knowledge/context_builder.py +43 -0
- labpilot_ai/knowledge/indexer.py +290 -0
- labpilot_ai/knowledge/search.py +99 -0
- labpilot_ai/label.png +0 -0
- labpilot_ai/lyse_ctrl/__init__.py +1 -0
- labpilot_ai/lyse_ctrl/h5_loader.py +50 -0
- labpilot_ai/lyse_ctrl/module_manager.py +55 -0
- labpilot_ai/lyse_ctrl/multi_runner.py +19 -0
- labpilot_ai/lyse_ctrl/result_store.py +55 -0
- labpilot_ai/lyse_ctrl/single_runner.py +22 -0
- labpilot_ai/main.py +26 -0
- labpilot_ai/optimizer/__init__.py +1 -0
- labpilot_ai/optimizer/auto_loop.py +259 -0
- labpilot_ai/optimizer/base.py +30 -0
- labpilot_ai/optimizer/bayesian.py +46 -0
- labpilot_ai/optimizer/experiment_loop.py +55 -0
- labpilot_ai/optimizer/grid_search.py +20 -0
- labpilot_ai/optimizer/history.py +27 -0
- labpilot_ai/optimizer/lyse_feedback.py +91 -0
- labpilot_ai/optimizer/objective.py +116 -0
- labpilot_ai/protocol/__init__.py +1 -0
- labpilot_ai/protocol/protocol_store.py +8 -0
- labpilot_ai/runmanager_ctrl/__init__.py +1 -0
- labpilot_ai/runmanager_ctrl/backend.py +86 -0
- labpilot_ai/runmanager_ctrl/scan_builder.py +9 -0
- labpilot_ai/safety/__init__.py +1 -0
- labpilot_ai/safety/audit_log.py +14 -0
- labpilot_ai/safety/validator.py +375 -0
- labpilot_ai/storage/__init__.py +1 -0
- labpilot_ai/storage/database.py +273 -0
- labpilot_ai/templates/configs/blacs_manual_registry.yaml +12 -0
- labpilot_ai/templates/configs/global_registry.yaml +17 -0
- labpilot_ai/templates/configs/lyse_registry.yaml +16 -0
- labpilot_ai/templates/configs/project_settings.yaml +56 -0
- labpilot_ai/templates/configs/safety_rules.yaml +2 -0
- labpilot_ai/templates/configs/voice_lexicon.yaml +8 -0
- labpilot_ai/templates/manual/00_overview/architecture.md +44 -0
- labpilot_ai/templates/manual/00_overview/project_structure.md +54 -0
- labpilot_ai/templates/manual/00_overview/quick_start.md +60 -0
- labpilot_ai/templates/manual/01_voice/cpu_gpu_stt.md +78 -0
- labpilot_ai/templates/manual/01_voice/cuda_troubleshooting.md +96 -0
- labpilot_ai/templates/manual/01_voice/lexicon_terms.md +44 -0
- labpilot_ai/templates/manual/01_voice/manual_recording.md +45 -0
- labpilot_ai/templates/manual/01_voice/wake_standby.md +34 -0
- labpilot_ai/templates/manual/02_ui/co_sequence.md +39 -0
- labpilot_ai/templates/manual/02_ui/command_diagnostics.md +44 -0
- labpilot_ai/templates/manual/02_ui/industrial_ui.md +42 -0
- labpilot_ai/templates/manual/03_safety_ai/action_schema.md +101 -0
- labpilot_ai/templates/manual/03_safety_ai/llm_client.md +51 -0
- labpilot_ai/templates/manual/03_safety_ai/safety_validator.md +48 -0
- labpilot_ai/templates/manual/04_labscript_interfaces/blacs.md +50 -0
- labpilot_ai/templates/manual/04_labscript_interfaces/lyse_data.md +68 -0
- labpilot_ai/templates/manual/04_labscript_interfaces/runmanager.md +51 -0
- labpilot_ai/templates/manual/05_analysis_optimizer/optimizer.md +109 -0
- labpilot_ai/templates/manual/05_analysis_optimizer/plotting_fitting.md +73 -0
- labpilot_ai/templates/manual/05_analysis_optimizer/protocol_designer.md +31 -0
- labpilot_ai/templates/manual/05_analysis_optimizer/protocol_report.md +40 -0
- labpilot_ai/templates/manual/06_configuration/directory.md +28 -0
- labpilot_ai/templates/manual/06_configuration/knowledge_sources.md +34 -0
- labpilot_ai/templates/manual/06_configuration/registry_editor.md +32 -0
- labpilot_ai/templates/manual/06_configuration/settings_and_registries.md +87 -0
- labpilot_ai/templates/manual/06_configuration/voice_settings.md +73 -0
- labpilot_ai/templates/manual/07_development/error_center.md +24 -0
- labpilot_ai/templates/manual/07_development/experiment_log.md +43 -0
- labpilot_ai/templates/manual/07_development/package_templates.md +38 -0
- labpilot_ai/templates/manual/07_development/packaging.md +69 -0
- labpilot_ai/templates/manual/07_development/tests.md +85 -0
- labpilot_ai/templates/manual/README.md +41 -0
- labpilot_ai/templates/plugins/multi_modules/example_multi.py +2 -0
- labpilot_ai/templates/plugins/single_modules/example_single.py +2 -0
- labpilot_ai/utils/__init__.py +1 -0
- labpilot_ai/utils/json_utils.py +18 -0
- labpilot_ai/utils/paths.py +20 -0
- labpilot_ai/voice/__init__.py +1 -0
- labpilot_ai/voice/cuda_paths.py +128 -0
- labpilot_ai/voice/diagnostics.py +138 -0
- labpilot_ai/voice/lexicon.py +138 -0
- labpilot_ai/voice/recorder.py +107 -0
- labpilot_ai/voice/stt_backend.py +172 -0
- labpilot_ai/voice/stt_worker.py +75 -0
- labpilot_ai/voice/vad.py +27 -0
- labpilot_ai/voice/wake_agent.py +32 -0
- labpilot_ai-0.1.2.dist-info/METADATA +150 -0
- labpilot_ai-0.1.2.dist-info/RECORD +121 -0
- labpilot_ai-0.1.2.dist-info/WHEEL +5 -0
- labpilot_ai-0.1.2.dist-info/entry_points.txt +2 -0
- labpilot_ai-0.1.2.dist-info/top_level.txt +1 -0
labpilot_ai/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
labpilot_ai/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _read_limited(path, max_chars):
|
|
6
|
+
path = Path(path)
|
|
7
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
8
|
+
truncated = len(text) > max_chars
|
|
9
|
+
if truncated:
|
|
10
|
+
text = text[:max_chars] + "\n\n# [LabPilot truncated this file for prompt budget]\n"
|
|
11
|
+
return text, truncated
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_co_sequence_prompt(
|
|
15
|
+
instruction,
|
|
16
|
+
sequence_path,
|
|
17
|
+
connection_table_path,
|
|
18
|
+
*,
|
|
19
|
+
project_context="",
|
|
20
|
+
max_file_chars=60000,
|
|
21
|
+
):
|
|
22
|
+
sequence_text, sequence_truncated = _read_limited(sequence_path, max_file_chars)
|
|
23
|
+
connection_text, connection_truncated = _read_limited(connection_table_path, max_file_chars)
|
|
24
|
+
return f"""
|
|
25
|
+
You are LabPilot Co-Sequence, a code editing assistant for labscript experiments.
|
|
26
|
+
You may propose changes ONLY to the selected sequence file and selected connection table file.
|
|
27
|
+
Do not propose changes to any other file. Do not run hardware. Do not include Markdown fences.
|
|
28
|
+
|
|
29
|
+
User instruction:
|
|
30
|
+
{instruction}
|
|
31
|
+
|
|
32
|
+
Project knowledge snippets:
|
|
33
|
+
{project_context or "No local knowledge snippets were retrieved."}
|
|
34
|
+
|
|
35
|
+
Selected editable files:
|
|
36
|
+
1. sequence_path={sequence_path}
|
|
37
|
+
truncated={sequence_truncated}
|
|
38
|
+
```python
|
|
39
|
+
{sequence_text}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
2. connection_table_path={connection_table_path}
|
|
43
|
+
truncated={connection_truncated}
|
|
44
|
+
```python
|
|
45
|
+
{connection_text}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Return strict JSON with this shape:
|
|
49
|
+
{{
|
|
50
|
+
"summary": "short summary",
|
|
51
|
+
"risk_level": "low|medium|high",
|
|
52
|
+
"files": [
|
|
53
|
+
{{"path": "...", "unified_diff": "unified diff for that exact file"}}
|
|
54
|
+
],
|
|
55
|
+
"consistency_checks": ["how sequence and connection table stay consistent"],
|
|
56
|
+
"color_tags": ["yellow: reason", "red: reason"],
|
|
57
|
+
"warnings": ["operator-facing warning"]
|
|
58
|
+
}}
|
|
59
|
+
|
|
60
|
+
Rules:
|
|
61
|
+
- Every file path must be exactly one of the two selected paths.
|
|
62
|
+
- Use unified diffs only. No full file replacement.
|
|
63
|
+
- Keep sequence and connection table device/channel/global names consistent.
|
|
64
|
+
- Prefer small, reviewable patches.
|
|
65
|
+
- If the request is unsafe or underspecified, return an empty files list and put the issue in warnings.
|
|
66
|
+
""".strip()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def mock_co_sequence_plan(instruction, sequence_path, connection_table_path):
|
|
70
|
+
return {
|
|
71
|
+
"summary": "Mock Co-Sequence did not modify code. Enable an API key to generate real diffs.",
|
|
72
|
+
"risk_level": "low",
|
|
73
|
+
"files": [],
|
|
74
|
+
"consistency_checks": [
|
|
75
|
+
"Mock mode keeps sequence and connection table unchanged.",
|
|
76
|
+
f"Sequence target: {Path(sequence_path).name if sequence_path else ''}",
|
|
77
|
+
f"Connection table target: {Path(connection_table_path).name if connection_table_path else ''}",
|
|
78
|
+
],
|
|
79
|
+
"color_tags": ["gray: mock"],
|
|
80
|
+
"warnings": ["No patch was generated in Mock LLM mode."],
|
|
81
|
+
"instruction": instruction,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def compact_plan_text(plan):
|
|
86
|
+
return json.dumps(plan or {}, ensure_ascii=False, indent=2)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
ALLOWED_ACTIONS = {
|
|
2
|
+
"set_global",
|
|
3
|
+
"set_blacs_manual",
|
|
4
|
+
"engage",
|
|
5
|
+
"get_globals",
|
|
6
|
+
"load_h5",
|
|
7
|
+
"load_h5_folder",
|
|
8
|
+
"run_single_lyse",
|
|
9
|
+
"run_multi_lyse",
|
|
10
|
+
"plot",
|
|
11
|
+
"fit",
|
|
12
|
+
"start_optimization",
|
|
13
|
+
"stop_optimization",
|
|
14
|
+
"tell_optimization_result",
|
|
15
|
+
"evaluate_optimization_result",
|
|
16
|
+
"generate_protocol",
|
|
17
|
+
"generate_report",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def action_summary(action: dict) -> str:
|
|
22
|
+
typ = action.get("type", "")
|
|
23
|
+
if typ in {"set_global", "set_blacs_manual"}:
|
|
24
|
+
return f"{typ}: {action.get('name')} = {action.get('value')!r}"
|
|
25
|
+
if typ == "start_optimization":
|
|
26
|
+
names = ", ".join((action.get("parameters") or {}).keys())
|
|
27
|
+
return f"{typ}: {action.get('method')} {action.get('mode')} {action.get('objective')} over {names}"
|
|
28
|
+
if typ in {"plot", "fit"}:
|
|
29
|
+
return f"{typ}: {action.get('plot_type', action.get('model', ''))}"
|
|
30
|
+
return typ
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
def advise_error(error_text: str) -> str:
|
|
2
|
+
text = (error_text or "").strip()
|
|
3
|
+
low = text.lower()
|
|
4
|
+
hints = []
|
|
5
|
+
|
|
6
|
+
if any(token in low for token in ["unicode", "utf-8", "codec", "mojibake", "encoding", "decode error", "�"]):
|
|
7
|
+
hints.append("Check source/document encoding. Save Python and Markdown files as UTF-8, then rerun compileall before release.")
|
|
8
|
+
if any(token in text for token in ["乱码", "鏄", "涓", "鎵", "寮"]):
|
|
9
|
+
hints.append("This looks like mojibake. Reopen the affected file as UTF-8, replace corrupted strings, and avoid mixed terminal encodings.")
|
|
10
|
+
if "not in global whitelist" in low or "global" in low and "whitelist" in low:
|
|
11
|
+
hints.append("Add the variable to configs/global_registry.yaml with type, range, risk, aliases, and description.")
|
|
12
|
+
if "importerror" in low or "modulenotfounderror" in low or "no module named" in low:
|
|
13
|
+
hints.append("Install the missing optional dependency, or disable the related feature in Settings before continuing.")
|
|
14
|
+
if "blacs" in low and ("connection" in low or "refused" in low or "bridge" in low or "localhost" in low):
|
|
15
|
+
hints.append("Check the BLACS localhost bridge, the registered channel name, and Mock BLACS mode before programming hardware.")
|
|
16
|
+
if "runmanager" in low and ("connection" in low or "timeout" in low or "remote" in low):
|
|
17
|
+
hints.append("Check that runmanager is running, remote access is available, and the timeout is long enough.")
|
|
18
|
+
if "h5py has already been imported" in low or "h5_lock" in low:
|
|
19
|
+
hints.append("Restart Python and ensure labpilot_ai.bootstrap_labscript.install_h5_lock() runs before importing h5py.")
|
|
20
|
+
if "cublas" in low or "cudnn" in low or "cuda runtime" in low:
|
|
21
|
+
hints.append("For GPU STT, run Diagnostics and verify CUDA/cuBLAS/cuDNN DLL paths; switch to CPU/int8 if the DLL stack is unstable.")
|
|
22
|
+
if "openmp" in low or "libiomp5md" in low or "omp: error" in low:
|
|
23
|
+
hints.append("Keep STT in isolated_release mode or CPU/int8 mode to isolate OpenMP runtime conflicts.")
|
|
24
|
+
if "pypdf" in low or "pdf import requires" in low or "pdf indexing requires" in low:
|
|
25
|
+
hints.append("Install PDF support with pip install -e .[docs] or pip install pypdf.")
|
|
26
|
+
if "module" in low and ("not registered" in low or "no module" in low or "path does not exist" in low):
|
|
27
|
+
hints.append("Register the single/multi analysis module in configs/lyse_registry.yaml and verify the module path.")
|
|
28
|
+
if "objective" in low and ("missing" in low or "unsafe" in low or "variable" in low):
|
|
29
|
+
hints.append("Choose an objective from lyse results or use a safe expression over registered result fields.")
|
|
30
|
+
if "scipy" in low or "curve_fit" in low:
|
|
31
|
+
hints.append("Install scipy fitting dependencies with pip install -e .[fit], or choose a simpler fit model.")
|
|
32
|
+
if "h5" in low and ("timeout" in low or "folder" in low or "not found" in low):
|
|
33
|
+
hints.append("Verify the H5 output folder, shot naming rule, and whether the experiment actually produced a new H5 file.")
|
|
34
|
+
if "knowledge" in low or "fts" in low or "sqlite" in low:
|
|
35
|
+
hints.append("Rebuild the Knowledge index and confirm the configured source folders are readable.")
|
|
36
|
+
|
|
37
|
+
if not hints:
|
|
38
|
+
hints.append("Check the traceback, then reproduce in Dry run or Mock mode before touching hardware.")
|
|
39
|
+
return "\n".join(f"- {hint}" for hint in hints)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def extract_json_object(text: str) -> dict:
|
|
5
|
+
text = (text or "").strip()
|
|
6
|
+
try:
|
|
7
|
+
return json.loads(text)
|
|
8
|
+
except json.JSONDecodeError:
|
|
9
|
+
pass
|
|
10
|
+
start = text.find("{")
|
|
11
|
+
end = text.rfind("}")
|
|
12
|
+
if start >= 0 and end > start:
|
|
13
|
+
return json.loads(text[start:end+1])
|
|
14
|
+
raise ValueError(f"无法解析 JSON:{text}")
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from .co_sequence_prompt import build_co_sequence_prompt, mock_co_sequence_plan
|
|
5
|
+
from .json_parser import extract_json_object
|
|
6
|
+
from .prompt_builder import build_command_prompt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LLMClient:
|
|
10
|
+
def __init__(self, api_key=None, base_url=None, model=None, use_thinking=True, mock=False):
|
|
11
|
+
self.api_key = api_key or os.getenv("DEEPSEEK_API_KEY") or os.getenv("OPENAI_API_KEY")
|
|
12
|
+
self.base_url = base_url or os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
|
|
13
|
+
self.model = model or os.getenv("DEEPSEEK_MODEL", "deepseek-v4-flash")
|
|
14
|
+
self.use_thinking = use_thinking
|
|
15
|
+
self.mock = mock
|
|
16
|
+
|
|
17
|
+
def parse_command(self, user_text: str, global_registry: dict, blacs_registry=None, lyse_registry=None, project_context=None) -> dict:
|
|
18
|
+
if self.mock or not self.api_key:
|
|
19
|
+
result = self._mock_parse(user_text, global_registry)
|
|
20
|
+
if project_context:
|
|
21
|
+
result["project_context_used"] = True
|
|
22
|
+
return result
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from openai import OpenAI
|
|
26
|
+
except ImportError as exc:
|
|
27
|
+
raise RuntimeError("OpenAI-compatible SDK is not installed. Install the project dependencies or enable Mock LLM.") from exc
|
|
28
|
+
|
|
29
|
+
client = OpenAI(api_key=self.api_key, base_url=self.base_url, timeout=60.0)
|
|
30
|
+
kwargs = dict(
|
|
31
|
+
model=self.model,
|
|
32
|
+
messages=[
|
|
33
|
+
{"role": "system", "content": build_command_prompt(global_registry, blacs_registry, lyse_registry, project_context=project_context)},
|
|
34
|
+
{"role": "user", "content": user_text},
|
|
35
|
+
],
|
|
36
|
+
stream=False,
|
|
37
|
+
temperature=0,
|
|
38
|
+
response_format={"type": "json_object"},
|
|
39
|
+
)
|
|
40
|
+
if self.use_thinking:
|
|
41
|
+
kwargs["reasoning_effort"] = "high"
|
|
42
|
+
kwargs["extra_body"] = {"thinking": {"type": "enabled"}}
|
|
43
|
+
response = client.chat.completions.create(**kwargs)
|
|
44
|
+
return extract_json_object(response.choices[0].message.content)
|
|
45
|
+
|
|
46
|
+
def _mock_parse(self, text: str, global_registry: dict) -> dict:
|
|
47
|
+
"""Small local parser for testing without API. It only handles common cases."""
|
|
48
|
+
actions = []
|
|
49
|
+
low = (text or "").lower()
|
|
50
|
+
no_run = any(token in low for token in ["do not run", "don't run", "no run", "not run", "dry run"])
|
|
51
|
+
no_run = no_run or any(token in (text or "") for token in ["不要运行", "不运行", "别运行", "不跑"])
|
|
52
|
+
|
|
53
|
+
if any(token in low for token in ["tof", "time of flight"]) or any(token in (text or "") for token in ["飞行时间"]):
|
|
54
|
+
m = re.search(r"(?:from|从)\s*([\d.]+)\s*(?:ms)?\s*(?:to|到)\s*([\d.]+)\s*(?:ms)?.*?(\d+)\s*(?:points|点)", text or "", re.I)
|
|
55
|
+
if m and "duration_tof_ms" in global_registry:
|
|
56
|
+
actions.append(
|
|
57
|
+
{
|
|
58
|
+
"type": "set_global",
|
|
59
|
+
"name": "duration_tof_ms",
|
|
60
|
+
"value": {"linspace": [float(m.group(1)), float(m.group(2)), int(m.group(3))]},
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
m = re.search(r"(?:tof|time of flight|飞行时间).*?(?:to|set to|设为|设置为|改成|=)\s*([\d.]+)", text or "", re.I)
|
|
65
|
+
if m and "duration_tof_ms" in global_registry:
|
|
66
|
+
actions.append({"type": "set_global", "name": "duration_tof_ms", "value": float(m.group(1))})
|
|
67
|
+
|
|
68
|
+
bool_map = {
|
|
69
|
+
"lyse_do_SG_F1_masked": ["sg mask", "遮罩"],
|
|
70
|
+
"lyse_update_centers_json": ["update centers", "centers_json", "centers json", "更新中心"],
|
|
71
|
+
"do_Rabi": ["rabi", "拉比"],
|
|
72
|
+
"do_pure": ["pure", "清除"],
|
|
73
|
+
"do_Ramsey": ["ramsey"],
|
|
74
|
+
}
|
|
75
|
+
for name, keys in bool_map.items():
|
|
76
|
+
if name in global_registry and any(key.lower() in low or key in (text or "") for key in keys):
|
|
77
|
+
val = not any(token in low for token in ["disable", "off", "turn off"]) and not any(
|
|
78
|
+
token in (text or "") for token in ["关闭", "关掉", "不要打开"]
|
|
79
|
+
)
|
|
80
|
+
actions.append({"type": "set_global", "name": name, "value": val})
|
|
81
|
+
|
|
82
|
+
if not no_run and (any(token in low for token in ["run", "submit", "engage"]) or any(token in (text or "") for token in ["运行", "跑一次", "提交"])):
|
|
83
|
+
actions.append({"type": "engage"})
|
|
84
|
+
if any(token in low for token in ["read", "current", "globals"]) or any(token in (text or "") for token in ["读取", "查看", "当前参数"]):
|
|
85
|
+
actions.append({"type": "get_globals"})
|
|
86
|
+
return {"actions": actions, "comment": "Mock LLM parser result. For serious use, enable API."}
|
|
87
|
+
|
|
88
|
+
def propose_co_sequence_patch(
|
|
89
|
+
self,
|
|
90
|
+
instruction: str,
|
|
91
|
+
sequence_path,
|
|
92
|
+
connection_table_path,
|
|
93
|
+
*,
|
|
94
|
+
project_context=None,
|
|
95
|
+
max_file_chars=60000,
|
|
96
|
+
) -> dict:
|
|
97
|
+
if self.mock or not self.api_key:
|
|
98
|
+
return mock_co_sequence_plan(instruction, sequence_path, connection_table_path)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
from openai import OpenAI
|
|
102
|
+
except ImportError as exc:
|
|
103
|
+
raise RuntimeError("OpenAI-compatible SDK is not installed. Install the project dependencies or enable Mock LLM.") from exc
|
|
104
|
+
|
|
105
|
+
client = OpenAI(api_key=self.api_key, base_url=self.base_url, timeout=90.0)
|
|
106
|
+
prompt = build_co_sequence_prompt(
|
|
107
|
+
instruction,
|
|
108
|
+
sequence_path,
|
|
109
|
+
connection_table_path,
|
|
110
|
+
project_context=project_context,
|
|
111
|
+
max_file_chars=max_file_chars,
|
|
112
|
+
)
|
|
113
|
+
kwargs = dict(
|
|
114
|
+
model=self.model,
|
|
115
|
+
messages=[
|
|
116
|
+
{"role": "system", "content": "You produce strict JSON patch plans for LabPilot Co-Sequence."},
|
|
117
|
+
{"role": "user", "content": prompt},
|
|
118
|
+
],
|
|
119
|
+
stream=False,
|
|
120
|
+
temperature=0,
|
|
121
|
+
response_format={"type": "json_object"},
|
|
122
|
+
)
|
|
123
|
+
if self.use_thinking:
|
|
124
|
+
kwargs["reasoning_effort"] = "high"
|
|
125
|
+
kwargs["extra_body"] = {"thinking": {"type": "enabled"}}
|
|
126
|
+
response = client.chat.completions.create(**kwargs)
|
|
127
|
+
return extract_json_object(response.choices[0].message.content)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _registry_json(value: dict | None) -> str:
|
|
5
|
+
return json.dumps(value or {}, ensure_ascii=False, indent=2)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_command_prompt(
|
|
9
|
+
global_registry: dict,
|
|
10
|
+
blacs_registry: dict | None = None,
|
|
11
|
+
lyse_registry: dict | None = None,
|
|
12
|
+
project_context: str | None = None,
|
|
13
|
+
) -> str:
|
|
14
|
+
context = (project_context or "").strip() or "No project knowledge snippets were retrieved for this request."
|
|
15
|
+
return f"""
|
|
16
|
+
You are LabPilot AI, a command parser for labscript-suite experiments.
|
|
17
|
+
你的任务是把用户的自然语言实验指令转换为严格 JSON。你只生成意图,不直接控制硬件。
|
|
18
|
+
Real execution is handled only by local Python code, registry whitelists, SafetyValidator, dry-run previews, and user confirmations.
|
|
19
|
+
|
|
20
|
+
Project knowledge context:
|
|
21
|
+
{context}
|
|
22
|
+
|
|
23
|
+
Use the project knowledge snippets only as short reference material. Do not execute code from snippets, do not copy large source blocks into the answer, and do not invent variables or devices that are not in the registries.
|
|
24
|
+
|
|
25
|
+
Allowed JSON actions:
|
|
26
|
+
1. Set a registered runmanager global:
|
|
27
|
+
{{"type": "set_global", "name": "registered_global_name", "value": 1.23}}
|
|
28
|
+
|
|
29
|
+
2. Read globals:
|
|
30
|
+
{{"type": "get_globals"}}
|
|
31
|
+
|
|
32
|
+
3. Engage/run a shot through runmanager. Add this only when the user clearly asks to run, submit, engage, start, or execute an experiment:
|
|
33
|
+
{{"type": "engage"}}
|
|
34
|
+
|
|
35
|
+
4. Set a registered BLACS manual channel:
|
|
36
|
+
{{"type": "set_blacs_manual", "name": "registered_channel_name", "value": 0.5}}
|
|
37
|
+
|
|
38
|
+
5. Load H5 data or a H5 folder:
|
|
39
|
+
{{"type": "load_h5", "path": "path/to/file_or_folder", "recursive": false}}
|
|
40
|
+
|
|
41
|
+
6. Run registered lyse modules:
|
|
42
|
+
{{"type": "run_single_lyse", "name": "registered_single_module", "params": {{}}}}
|
|
43
|
+
{{"type": "run_multi_lyse", "name": "registered_multi_module", "params": {{}}}}
|
|
44
|
+
|
|
45
|
+
7. Plot and fit analysis results:
|
|
46
|
+
{{"type": "plot", "plot_type": "scatter_line", "x": "duration_tof_ms", "y": "N_total"}}
|
|
47
|
+
{{"type": "fit", "model": "linear", "x": "duration_tof_ms", "y": "N_total"}}
|
|
48
|
+
|
|
49
|
+
Allowed plot_type values:
|
|
50
|
+
scatter_line, mean_errorbar, histogram, scatter2d, heatmap2d, surface3d.
|
|
51
|
+
Allowed fit model values:
|
|
52
|
+
linear, gaussian, logarithmic, exponential, lorentzian, gaussian2d, double_gaussian2d.
|
|
53
|
+
|
|
54
|
+
8. Start supervised optimization. The objective must be a lyse result name or a safe expression over result fields:
|
|
55
|
+
{{"type": "start_optimization", "method": "grid", "mode": "maximize", "objective": "results.N_total", "parameters": {{"duration_tof_ms": {{"min": 5, "max": 20, "points": 6}}}}, "max_iterations": 6, "repeats": 1, "auto_loop": false, "run_checked_modules": true, "poll_interval_s": 1.0, "h5_timeout_s": 120.0, "generate_report_on_complete": false}}
|
|
56
|
+
|
|
57
|
+
9. Stop or feed back optimization:
|
|
58
|
+
{{"type": "stop_optimization"}}
|
|
59
|
+
{{"type": "tell_optimization_result", "source": "latest_h5", "run_checked_modules": true}}
|
|
60
|
+
{{"type": "tell_optimization_result", "values": {{"N_total": 1.23}}}}
|
|
61
|
+
|
|
62
|
+
10. Generate suggestions or reports only:
|
|
63
|
+
{{"type": "generate_protocol", "prompt": "short protocol-design request"}}
|
|
64
|
+
{{"type": "generate_report", "title": "short report title", "include_errors": true, "include_knowledge_context": true, "include_optimizer_history": true}}
|
|
65
|
+
|
|
66
|
+
runmanager globals whitelist:
|
|
67
|
+
{_registry_json(global_registry)}
|
|
68
|
+
|
|
69
|
+
BLACS manual whitelist:
|
|
70
|
+
{_registry_json(blacs_registry)}
|
|
71
|
+
|
|
72
|
+
lyse modules:
|
|
73
|
+
{_registry_json(lyse_registry)}
|
|
74
|
+
|
|
75
|
+
Output format:
|
|
76
|
+
{{
|
|
77
|
+
"actions": [
|
|
78
|
+
{{"type": "..."}}
|
|
79
|
+
],
|
|
80
|
+
"comment": "short bilingual explanation if useful"
|
|
81
|
+
}}
|
|
82
|
+
|
|
83
|
+
Hard rules:
|
|
84
|
+
- Return strict JSON only. Do not use Markdown or code fences.
|
|
85
|
+
- Every action must use the field name "type"; never use the old field name "action".
|
|
86
|
+
- Only use names that appear in the whitelists above.
|
|
87
|
+
- If the user asks for a parameter that is not registered, do not create a set_global/set_blacs_manual action for it; explain in comment that it must be added to the registry first.
|
|
88
|
+
- bool values must be true or false. Accepted user wording includes on/off, enable/disable, 打开/关闭, 是/否, 启用/禁用.
|
|
89
|
+
- Float and int values must be JSON numbers.
|
|
90
|
+
- Array scans may use a JSON list, {{"linspace": [start, stop, num]}}, or {{"arange": [start, stop, step]}}.
|
|
91
|
+
- Optimization parameters must be registered runmanager globals and must include min, max, and points.
|
|
92
|
+
- Do not add engage if the user only asks to parse, preview, design, explain, optimize setup, or edit code.
|
|
93
|
+
- Co-Sequence code editing is handled by a separate page; command parsing must not generate file patches.
|
|
94
|
+
- Real hardware actions remain blocked by SafetyValidator and user confirmation.
|
|
95
|
+
""".strip()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
def draft_protocol_suggestion(prompt: str, known_globals: dict | None = None, project_context: str | None = None) -> str:
|
|
2
|
+
known_globals = known_globals or {}
|
|
3
|
+
names = ", ".join(list(known_globals)[:12]) or "no registered globals yet"
|
|
4
|
+
text = (prompt or "").strip()
|
|
5
|
+
context = (project_context or "").strip()
|
|
6
|
+
parts = [
|
|
7
|
+
"# Protocol suggestion",
|
|
8
|
+
"This page generates an experimental design suggestion only; it does not execute hardware actions.",
|
|
9
|
+
f"Input summary: {text[:1200]}",
|
|
10
|
+
]
|
|
11
|
+
if context:
|
|
12
|
+
parts.append(f"Relevant project context:\n{context[:1800]}")
|
|
13
|
+
parts.extend(
|
|
14
|
+
[
|
|
15
|
+
f"Available controllable globals include: {names}.",
|
|
16
|
+
"Recommended workflow:\n"
|
|
17
|
+
"1. Convert the scientific goal into a measurable lyse result.\n"
|
|
18
|
+
"2. Register every adjustable runmanager variable with conservative ranges.\n"
|
|
19
|
+
"3. Run a small grid scan in Dry run and then on hardware.\n"
|
|
20
|
+
"4. Promote the best region to Bayesian optimization only after the lyse result is stable.\n",
|
|
21
|
+
]
|
|
22
|
+
)
|
|
23
|
+
return "\n\n".join(parts)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
TEXT_EXTENSIONS = {".txt", ".md", ".markdown"}
|
|
5
|
+
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".bmp", ".gif", ".tif", ".tiff", ".webp"}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def read_text_document(path):
|
|
9
|
+
path = Path(path)
|
|
10
|
+
try:
|
|
11
|
+
return path.read_text(encoding="utf-8")
|
|
12
|
+
except UnicodeDecodeError:
|
|
13
|
+
return path.read_text(encoding="utf-8-sig")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_pdf_text(path):
|
|
17
|
+
path = Path(path)
|
|
18
|
+
try:
|
|
19
|
+
from pypdf import PdfReader
|
|
20
|
+
except Exception as exc:
|
|
21
|
+
raise RuntimeError("PDF import requires labpilot-ai[docs] or `pip install pypdf`.") from exc
|
|
22
|
+
|
|
23
|
+
reader = PdfReader(str(path))
|
|
24
|
+
parts = []
|
|
25
|
+
for index, page in enumerate(reader.pages, start=1):
|
|
26
|
+
text = page.extract_text() or ""
|
|
27
|
+
if text.strip():
|
|
28
|
+
parts.append(f"[PDF page {index}]\n{text.strip()}")
|
|
29
|
+
return "\n\n".join(parts)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def import_protocol_file(path):
|
|
33
|
+
path = Path(path)
|
|
34
|
+
suffix = path.suffix.lower()
|
|
35
|
+
if suffix in TEXT_EXTENSIONS:
|
|
36
|
+
return read_text_document(path)
|
|
37
|
+
if suffix == ".pdf":
|
|
38
|
+
return read_pdf_text(path)
|
|
39
|
+
raise ValueError(f"Unsupported protocol import type: {suffix or path.name}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def describe_image_attachment(path, note=""):
|
|
43
|
+
path = Path(path)
|
|
44
|
+
if path.suffix.lower() not in IMAGE_EXTENSIONS:
|
|
45
|
+
raise ValueError(f"Unsupported image attachment type: {path.suffix or path.name}")
|
|
46
|
+
text = f"Image attachment: {path.name}\nPath: {path}"
|
|
47
|
+
if note:
|
|
48
|
+
text += f"\nHuman note: {note}"
|
|
49
|
+
return text
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_protocol_prompt(text, attachments=None):
|
|
53
|
+
sections = [(text or "").strip()]
|
|
54
|
+
attachments = [str(item).strip() for item in (attachments or []) if str(item).strip()]
|
|
55
|
+
if attachments:
|
|
56
|
+
sections.append("Attachments and operator notes:\n" + "\n\n".join(attachments))
|
|
57
|
+
return "\n\n".join([section for section in sections if section])
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|