Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
database/domain_datasets.tar.gz filter=lfs diff=lfs merge=lfs -text

# Preserve imported scientific source formatting and provenance hashes.
src/matcreator/xrd/vendor/OAW_XRDfit/** whitespace=-blank-at-eol,-blank-at-eof
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,8 @@ POT_GGA_PAW_PBE/

# Front end
web/vite-frontend/dist/
web/vite-frontend/node_modules/
web/vite-frontend/node_modules/

# Local XRD runtime acceptance artifacts (may contain user inputs).
.local-review-logs/
.local-review-backup/
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ where = ["src"]
"agents/subagent.yaml",
"guides/*.md",
"question_templates/*.json",
"xrd/SOURCE.json",
"xrd/JEV_SOURCE.json",
"xrd/README.md",
"xrd/vendor/OAW_XRDfit/**/*",
]

[tool.semantic_release]
Expand Down
46 changes: 46 additions & 0 deletions script/xrd_local_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Small synthetic local API acceptance. Run with the XRD scientific interpreter."""
import base64, json, math, os, time, urllib.request
from pathlib import Path
from pymatgen.core import Lattice, Structure
from pymatgen.analysis.diffraction.xrd import XRDCalculator
from pymatgen.io.cif import CifWriter

URL=os.environ.get('MC_XRD_API_URL','http://127.0.0.1:8001/api/xrd').rstrip('/')
def api(path,body=None):
request=urllib.request.Request(URL+path,data=None if body is None else json.dumps(body).encode(),headers={'Content-Type':'application/json'})
with urllib.request.urlopen(request,timeout=120) as response:return json.load(response)
def main():
structure=Structure.from_spacegroup('Fd-3m',Lattice.cubic(5.43),['Si'],[[0,0,0]])
pattern=XRDCalculator(wavelength=1.540593).get_pattern(structure,two_theta_range=(15,75))
points=[[15+i*.03,30+sum(20*y*math.exp(-((15+i*.03-x)/.10)**2) for x,y in zip(pattern.x,pattern.y))] for i in range(2001)]
references='PDF#SYNTHETIC: software acceptance\nSynthetic Silicon\nSi\nLambda = 1.540593\n'+'\n'.join(f'{x} {d} {y} (1 1 1)' for x,d,y in zip(pattern.x,pattern.d_hkls,pattern.y))
owner=api('/workspaces',{'name':'合成 Si · 接入验收(非实验)'})['id']
def command(action,args={}):return api('/workspaces/'+owner+'/command',{'action':action,'arguments':args})
def upload(kind,name,text):
target=command('ensure_input',{'kind':kind})
command('document',{'target':target,'action':'import','revision':0,'arguments':{'filename':name,'source_base64':base64.b64encode(text.encode()).decode()}})
return target
upload('pattern','synthetic-silicon.csv','\n'.join(f'{x},{y}' for x,y in points))
reference=upload('reference','synthetic-silicon-reference.txt',references)
cif=upload('cif','synthetic-silicon.cif',str(CifWriter(structure)))
command('document',{'target':cif,'action':'associate','revision':1,'arguments':{'reference_node_id':reference}})
receipts={}
for stage in ['search','preopt','fit']:
config={'library_engine':'native','workflow_stage':stage,'iterations':1,'preopt_max_nfev':1,'low_angle':15,'high_angle':75}
if stage!='search':config.update(workflow_match_run_id=receipts['search']['run_id'],selected_candidate_ids=[reference])
if stage=='fit':config['workflow_preopt_run_id']=receipts['preopt']['run_id']
receipt=command('start',{'request_id':'synthetic-acceptance-'+stage,'config':config});receipts[stage]=receipt
deadline=time.monotonic()+180
while time.monotonic()<deadline:
info=api('/workspaces/'+owner)
if info['status'] not in {'running','waiting'}:break
time.sleep(1)
else:raise RuntimeError('Synthetic stage timeout; inspect existing run, do not resubmit: '+receipt['run_id'])
if info['error']:raise RuntimeError(info['error'])
result=info['details']['result'] if stage=='search' else info['details']['workflow'][stage]['result']
if stage!='search':assert result['candidates'][0]['status']!='failed',result['candidates'][0]
print(json.dumps({'workspace_id':owner,'stage':stage,'run_id':receipt['run_id'],'status':result.get('status','completed'),'candidate_status':result['candidates'][0].get('status'),'converged':result['candidates'][0].get('converged')},ensure_ascii=False),flush=True)
output=Path('.local-review-logs/xrd-smoke-result.json')
output.parent.mkdir(parents=True,exist_ok=True)
output.write_text(json.dumps({'workspace_id':owner,'receipts':receipts},indent=2),encoding='utf-8')
if __name__=='__main__':main()
4 changes: 4 additions & 0 deletions src/matcreator/agents/execution_agent/step_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from google.adk.agents import LlmAgent
from google.adk.models.lite_llm import LiteLlm
from google.adk.tools.function_tool import FunctionTool
from matcreator.tools.xrd_tools import xrd_list_workspaces, xrd_get_results, xrd_action
from google.adk.tools.tool_context import ToolContext
from pydantic import BaseModel, Field, ValidationError, field_validator, model_validator

Expand Down Expand Up @@ -306,6 +307,9 @@ def build_step_executor_agent(llm_card: LLMCard) -> LlmAgent:
FunctionTool(get_user_skills_root),
FunctionTool(run_python),
FunctionTool(run_bash),
FunctionTool(xrd_list_workspaces),
FunctionTool(xrd_get_results),
FunctionTool(xrd_action),
FunctionTool(submit_bohr_sandbox),
FunctionTool(create_remote_job_group),
FunctionTool(submit_bohr_batchjob),
Expand Down
4 changes: 4 additions & 0 deletions src/matcreator/agents/thinking_agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from google.adk.agents import LlmAgent
from google.adk.models.lite_llm import LiteLlm
from google.adk.tools.function_tool import FunctionTool
from matcreator.tools.xrd_tools import xrd_list_workspaces, xrd_get_results, xrd_action
from google.adk.tools.tool_context import ToolContext
from google.adk.agents.callback_context import CallbackContext

Expand Down Expand Up @@ -496,6 +497,9 @@ def before_agent_callback(callback_context: CallbackContext) -> None:
FunctionTool(get_user_skills_root),
FunctionTool(run_python),
FunctionTool(run_bash),
FunctionTool(xrd_list_workspaces),
FunctionTool(xrd_get_results),
FunctionTool(xrd_action),
FunctionTool(run_flash_step),
FunctionTool(read_session_log),
FunctionTool(list_remote_jobs),
Expand Down
17 changes: 17 additions & 0 deletions src/matcreator/skills/xrd/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
name: xrd
description: "Operate the local MC XRD workbench through its native Agent tools: import inputs, search reference libraries, refine candidate structures, construct multiphase combinations, and read audited results."
---

# XRD workbench

Use `xrd_list_workspaces`, `xrd_get_results`, and `xrd_action`. These use the same authoritative MC Web service as the XRD UI. Do not execute an independent copy of the engine or edit workspace JSON/SQLite directly.

1. Read the workspace ID supplied by the user, or list workspaces and identify the relevant one by evidence. Read existing state first.
2. Follow MC's current mode and approval rules. Reading results is not authorization to start a new analysis. In Standard mode include analysis execution in the approved plan.
3. For a single-phase workflow use `start` with one stable `request_id` and a `config` object. Reuse the same request ID after a timeout. Use `workflow_stage` search, then preopt, then fit. For later stages provide the verified `workflow_match_run_id`, selected candidate IDs, and (for fit) `workflow_preopt_run_id`.
4. Poll `xrd_get_results`. If running/waiting, reattach; do not launch a replacement. The service stores worker heartbeats, immutable input snapshots, run IDs, source hashes, stage status, frame histories and SQLite journal entries.
5. Multiphase: `multiphase_start` prepares a bounded harness. For each construction request `multiphase_decision` with `draft_candidate_ids`; choose only the returned legal ADD/SUBMIT actions. Preserve each decision token and construction path. Use `multiphase_evaluate` with `candidate_ids`, `reason`, `selection_token`, `construction_path`. Do not invent metrics or bypass duplicate/pool checks. Exhaust the configured Agent budget, then call `multiphase_baseline`, then `multiphase_finish`. Independent baseline evidence must not guide prior Agent choices. `multiphase_review` takes `run_id` and distinct measured `combinations`.
6. Read `history_list`, `history_inspect` and `history_records` through xrd_action for lineage. Keep execution status, input integrity, convergence/scientific acceptance and claim scope separate. Candidate matches are hypotheses; profile contributions are not mass fractions. A lower Rwp does not establish phase identity. Retain failed candidates, stopping reasons and fallback structures.

Input preparation uses `ensure_input` (kind pattern/reference/cif/library), followed by `document` with action import/configure/associate, arguments, expected revision and target ID. An import requires filename and base64-encoded content; a library mount requires its existing local .sq path and slot. Never upload local inputs to an external service. COD fetches only public candidate IDs.
43 changes: 43 additions & 0 deletions src/matcreator/tools/xrd_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""XRD tools share MC Web's authoritative service, never spawn duplicate workers."""
import json, os, re
import httpx
from matcreator.ports import get_web_port

def _url():
return f'http://127.0.0.1:{get_web_port()}/api/xrd'
def _compact(value):
if isinstance(value,dict):
return {k:_compact(v) for k,v in value.items() if k not in {'archive','source_base64','points','observed','calculated','frame_sets','frames','plot'}}
if isinstance(value,list): return [_compact(v) for v in value]
return value
async def _request(path='',body=None):
async with httpx.AsyncClient(timeout=600) as client:
r=await client.get(_url()+path) if body is None else await client.post(_url()+path,json=body)
r.raise_for_status();return r.json()
async def xrd_list_workspaces() -> dict:
"""List persistent XRD workspaces. Reuse the user-specified workspace; never guess input identity."""
return {'workspaces':await _request('/workspaces')}
async def xrd_get_results(workspace_id: str) -> dict:
"""Read XRD execution status, candidates, convergence and lineage. A low residual does not prove phase identity or mass fraction."""
if not re.fullmatch(r"[A-Za-z0-9_-]{1,100}", workspace_id): raise ValueError("Invalid workspace_id")
info=await _request('/workspaces/'+workspace_id)
return {'workspace_id':workspace_id,**_compact(info),'multiphase':_compact(await _request('/workspaces/'+workspace_id+'/multiphase'))}
async def xrd_action(workspace_id: str, action: str, arguments_json: str='{}') -> dict:
"""Operate the XRD workbench after user authorization / an approved Standard plan.

Actions: start (request_id REQUIRED, config optional; workflow_stage search/preopt/fit),
stop, update_config, ensure_input (kind pattern/reference/cif/library), document
(action import/configure/associate, arguments, revision, target),
history_list/history_inspect/history_records, multiphase_start,
multiphase_decision (draft_candidate_ids), multiphase_evaluate (candidate_ids,
reason, selection_token, construction_path), multiphase_baseline,
multiphase_finish, multiphase_review (run_id, combinations), multiphase_stop.
Start is idempotent: reuse the SAME request_id after timeout/reconnect. Poll
xrd_get_results; never resubmit a running or detached job. Multiphase must use
returned decision tokens and measured evidence. Exhaust Agent budget before
baseline, then finish. Results retain source/run IDs, hashes and claim scope.
"""
if not re.fullmatch(r"[A-Za-z0-9_-]{1,100}", workspace_id): raise ValueError("Invalid workspace_id")
arguments=json.loads(arguments_json)
if not isinstance(arguments,dict): raise ValueError('arguments_json must be an object')
return _compact(await _request('/workspaces/'+workspace_id+'/command',{'action':action,'arguments':arguments}))
5 changes: 5 additions & 0 deletions src/matcreator/xrd/JEV_SOURCE.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"source": "https://github.com/theAfish/open-agent-world/blob/eb31455/backend/agents/typesafe.py",
"sha256": "350a93b0a9c8827c09cdface3273be86ced25662ae5d942022a33df080b713ef",
"adaptation": "MC event types and scoped service provider; original ADD/SUBMIT controller and response validation retained"
}
74 changes: 74 additions & 0 deletions src/matcreator/xrd/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# MC XRD integration

The OAW XRD implementation is vendored from `theAfish/open-agent-world` at
`eb31455`; original source hashes and checkout identity are in `SOURCE.json`.
The scientific engine license is retained in `vendor/OAW_XRDfit`.

## Entry points

- MC tab bar **+ → XRD 工作台** creates an independent tab and workspace, including
when another XRD tab is open. Workspaces and history persist independently of
chat sessions. Import an experimental spectrum from the input card or canvas;
mount a local reference database from the input section.
- A draggable edge button opens one movable, resizable floating area with the
spectrum above the crystal structure. Both follow the selected result.
Structure views mount MC's existing `StructureViewer.svelte` in read-only XRD
mode. The original OAW spectrum, frame cursor and workflow views are retained.
- MC thinking and execution agents have `xrd_list_workspaces`, `xrd_get_results`
and `xrd_action`. These are native ADK FunctionTools backed by the same local
Web service as the UI, not a standalone MCP server. Read the bundled XRD skill
for request IDs, candidate decision tokens and scientific claim boundaries.
- Result discussion prepares a prompt with the workspace ID in Chat; the user
sends it. Merely opening XRD does not invoke an LLM or start an analysis.

## Runtime and persistence

`MATCREATOR_XRD_PYTHON` selects a Python with numpy/scipy/pymatgen/ASE and the
OAW engine dependencies. `OAW_XRD_ROOT` identifies the existing scientific
runtime (including optional QualX). The local review launcher uses the existing
XRD environment and starts all three MC services with hidden windows.

Data live under `MATCREATOR_XRD_HOME`, defaulting to `MATCREATOR_HOME/xrd`.
Each workspace has revisioned documents and a SQLite journal. Worker input
snapshots, hashes, logs and terminal status live under `runs`. Single-stage
workers are supervised outside the Web request and completed detached workers
are reconciled on reconnect. Unknown live ownership blocks duplicate starts.
Multiphase interruptions retain history and are reported as interrupted; they
are not silently replayed. Local mode only; server mode rejects these routes.

## Verification

Run `pytest tests/test_xrd_*.py` with MC's Python and
`MC_TEST_XRD_PYTHON` pointing to the scientific interpreter. The tests cover
immutable snapshots, numerical recovery on synthetic mixtures, bounded Jev
decisions, cancellation, HTTP argument contracts and Windows atomic-save locks.
Run `npm test` and `npm run build` from `web/vite-frontend` for UI checks.

`script/xrd_local_smoke.py` creates a clearly labelled synthetic Si workspace
and exercises search, preoptimization and profile fitting through the local
API. It requires the scientific interpreter. Set `MC_XRD_API_URL` to the
running API base URL; the default is `http://127.0.0.1:8001/api/xrd`.

The local browser acceptance on 2026-10-15 completed a real Jev search over
27 parseable COD candidates, with 24 distinct evaluated combinations, followed
by joint refinement of the highest search-score combination. The review
completed 20 iterations with `converged=false`. This validates the software
path, not phase identity, purity, optimizer superiority or mass fractions.
Experimental input, COD databases, API credentials and local audit logs are
not distributed in this repository.

QualX is optional and must be installed separately under `OAW_XRD_ROOT` for
QualX database searches. The native peak matcher works with imported reference
tables. The pinned scientific requirements are recorded in
`vendor/OAW_XRDfit/requirements.txt`; use a separate compatible Python
environment rather than replacing MC's runtime dependencies.

## MC TypeSafe / Jev controller

The XRD header's **Jev** button configures the model ID (default `jev-1.13.0`), HTTPS base URL and credential. A saved API key is stored in the local MC XRD home `jev-config.json`; the read endpoint returns only `configured`, never the key. Alternatively set `TYPESAFE_API_KEY` (or the configured environment-variable name) before starting Web/API. This is a TypeSafe `/v1/systemone` connection, not an OpenAI chat endpoint.

Select **TypeSafe / Jev** in the multiphase run settings, then start. The Web/API process owns the bounded controller. It calls the existing workspace-scoped decision/evaluate/baseline/finish operations, validates the full ADD/SUBMIT construction, preserves persistent cloud-request reservations, and never retries or silently substitutes another optimizer after an API failure. Stop cancels the controller and numerical worker. Interrupted runs are retained for inspection and are not automatically restarted.

Only compact candidate/peak evidence and measured trial summaries are sent to Jev; BO results are withheld during selection. Model responses, probabilities, usage, latency and construction tokens are recorded in `<workspace>/<run_id>.jev.jsonl`; accepted construction paths also remain in the numerical harness records. Review recommendations do not start expensive joint refinement automatically.

Port provenance is in `JEV_SOURCE.json`. Tests cover mocked HTTP protocol, workspace scope, cancellation, duplicate start prevention and redacted configuration. Real cloud acceptance requires a valid credential; mocked responses are not scientific or provider-availability validation.
Loading
Loading