Spaces:
Running
Running
U1-R2A Unified Auto-Method UI + Four-Language Copy
#126
by GuXiaopeng - opened
- app.py +321 -94
- core/advanced_method_registry.py +340 -0
- i18n.py +191 -19
- task_executor.py +30 -4
app.py
CHANGED
|
@@ -30,8 +30,12 @@ from managed_auth import (
|
|
| 30 |
start_generation_task,
|
| 31 |
)
|
| 32 |
from i18n import (
|
| 33 |
-
LANGUAGE_OPTIONS,
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
)
|
| 36 |
from smartpaper_restore_bridge import edge_bridge
|
| 37 |
|
|
@@ -73,6 +77,12 @@ def inject_file_uploader_i18n(language: str) -> None:
|
|
| 73 |
payload = {
|
| 74 |
"browse": tr(language, "browse_files"),
|
| 75 |
"uploaders": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
tr(language, "upload_reference"): {
|
| 77 |
"title": tr(language, "drop_docs"),
|
| 78 |
"limit": tr(language, "limit_docs"),
|
|
@@ -294,6 +304,9 @@ for key, default in {
|
|
| 294 |
"sp_last_ui_task_status": "",
|
| 295 |
"sp_scroll_to_generation_action": False,
|
| 296 |
"sp_completed_download_scroll_task_id": "",
|
|
|
|
|
|
|
|
|
|
| 297 |
}.items():
|
| 298 |
if key not in st.session_state:
|
| 299 |
st.session_state[key] = default
|
|
@@ -435,6 +448,23 @@ def _task_mode(task: dict) -> str:
|
|
| 435 |
return "amj" if mode == "amj" else "asq"
|
| 436 |
|
| 437 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
def _task_output_path(task: dict) -> Path | None:
|
| 439 |
task_id = str(task.get("task_id") or "").strip()
|
| 440 |
filename = os.path.basename(str(task.get("output_filename") or "").strip())
|
|
@@ -462,7 +492,100 @@ def _save_uploaded(items, inputs_dir: Path, prefix: str) -> list[str]:
|
|
| 462 |
return saved
|
| 463 |
|
| 464 |
|
| 465 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
task_id = str(task.get("task_id") or "").strip()
|
| 467 |
numeric_id = (st.session_state.get("sp_user") or {}).get("numeric_id")
|
| 468 |
if not task_id or not isinstance(numeric_id, int) or numeric_id <= 0:
|
|
@@ -471,19 +594,23 @@ def _persist_task_request(task: dict, mode: str, lang_code: str) -> dict:
|
|
| 471 |
inputs_dir = task_dir / "inputs"
|
| 472 |
inputs_dir.mkdir(parents=True, exist_ok=True)
|
| 473 |
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
|
| 483 |
request_data = {
|
| 484 |
"task_id": task_id,
|
| 485 |
"user_id": numeric_id,
|
| 486 |
"mode": mode,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
"interface_language": lang_code,
|
| 488 |
"target_lang": get_target_language(lang_code),
|
| 489 |
"output_suffix": get_output_suffix(lang_code),
|
|
@@ -502,7 +629,6 @@ def _persist_task_request(task: dict, mode: str, lang_code: str) -> dict:
|
|
| 502 |
_atomic_json(task_dir / "request.json", request_data)
|
| 503 |
return request_data
|
| 504 |
|
| 505 |
-
|
| 506 |
def _pid_alive(pid: int) -> bool:
|
| 507 |
if not isinstance(pid, int) or pid <= 0:
|
| 508 |
return False
|
|
@@ -644,8 +770,36 @@ def _process_pending_launch(lang_code: str) -> None:
|
|
| 644 |
pending = st.session_state.get("sp_pending_launch")
|
| 645 |
if not isinstance(pending, dict):
|
| 646 |
return
|
| 647 |
-
|
|
|
|
|
|
|
| 648 |
idempotency_key = str(pending.get("idempotency_key") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 649 |
projection = get_product_ui_projection()
|
| 650 |
selected_product_tier = (
|
| 651 |
str(st.session_state.get("sp_product_tier") or "").strip().upper()
|
|
@@ -655,7 +809,7 @@ def _process_pending_launch(lang_code: str) -> None:
|
|
| 655 |
try:
|
| 656 |
response = start_generation_task(
|
| 657 |
idempotency_key=idempotency_key,
|
| 658 |
-
mode=
|
| 659 |
interface_language=lang_code,
|
| 660 |
target_words=int(st.session_state.get("sp_words") or APP_CONFIG["target_words_default"]),
|
| 661 |
product_tier=selected_product_tier,
|
|
@@ -684,10 +838,20 @@ def _process_pending_launch(lang_code: str) -> None:
|
|
| 684 |
return
|
| 685 |
st.session_state["sp_generation_task"] = task
|
| 686 |
st.session_state["sp_last_ui_task_status"] = str(task.get("status") or "")
|
|
|
|
| 687 |
try:
|
| 688 |
request_path = _task_dir(task_id) / "request.json"
|
| 689 |
if not request_path.is_file():
|
| 690 |
-
_persist_task_request(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 691 |
_launch_executor(task_id)
|
| 692 |
except Exception as exc:
|
| 693 |
st.session_state["sp_pending_launch"] = None
|
|
@@ -1191,9 +1355,9 @@ def _localized_terminal_task_error(task: dict, lang_code: str) -> tuple[str, str
|
|
| 1191 |
return detail or code or tr(lang_code, "node_error"), ""
|
| 1192 |
|
| 1193 |
|
| 1194 |
-
def _inject_generation_action_scroll(mode: str) -> None:
|
| 1195 |
-
"""Scroll immediately to the
|
| 1196 |
-
target_id = "sp-generation-action
|
| 1197 |
components.html(
|
| 1198 |
f"""
|
| 1199 |
<script>
|
|
@@ -1222,8 +1386,7 @@ def _scroll_to_generation_action_once() -> None:
|
|
| 1222 |
if not bool(st.session_state.get("sp_scroll_to_generation_action")):
|
| 1223 |
return
|
| 1224 |
st.session_state["sp_scroll_to_generation_action"] = False
|
| 1225 |
-
|
| 1226 |
-
fallback_id = "sp-generation-action-asq" if mode.lower() == "asq" else "sp-generation-action-amj"
|
| 1227 |
components.html(
|
| 1228 |
f"""
|
| 1229 |
<script>
|
|
@@ -1253,35 +1416,6 @@ def _scroll_to_generation_action_once() -> None:
|
|
| 1253 |
)
|
| 1254 |
|
| 1255 |
|
| 1256 |
-
def _select_active_task_tab(task: dict, lang_code: str) -> None:
|
| 1257 |
-
"""Keep the existing visual tab aligned with the immutable task mode after rerun/refresh."""
|
| 1258 |
-
status = str((task or {}).get("status") or "")
|
| 1259 |
-
mode = str((task or {}).get("mode") or "").strip().lower()
|
| 1260 |
-
if mode not in {"amj", "asq"}:
|
| 1261 |
-
return
|
| 1262 |
-
if status not in (ACTIVE_TASK_STATUSES | {"resumable", "completed", "failed", "cancelled"}):
|
| 1263 |
-
return
|
| 1264 |
-
label = tr(lang_code, "tab_asq" if mode == "asq" else "tab_amj")
|
| 1265 |
-
components.html(
|
| 1266 |
-
f"""
|
| 1267 |
-
<script>
|
| 1268 |
-
(() => {{
|
| 1269 |
-
const parentDocument = window.parent.document;
|
| 1270 |
-
const expected = {json.dumps(label)};
|
| 1271 |
-
const select = () => {{
|
| 1272 |
-
const tabs = Array.from(parentDocument.querySelectorAll('[role="tab"]'));
|
| 1273 |
-
const target = tabs.find((node) => (node.textContent || '').trim() === expected.trim());
|
| 1274 |
-
if (target && target.getAttribute('aria-selected') !== 'true') target.click();
|
| 1275 |
-
}};
|
| 1276 |
-
[0, 80, 220, 500, 900].forEach((delay) => window.parent.setTimeout(select, delay));
|
| 1277 |
-
}})();
|
| 1278 |
-
</script>
|
| 1279 |
-
""",
|
| 1280 |
-
height=0,
|
| 1281 |
-
width=0,
|
| 1282 |
-
)
|
| 1283 |
-
|
| 1284 |
-
|
| 1285 |
def render_workspace_controls(user):
|
| 1286 |
top_toolbar = st.container()
|
| 1287 |
with top_toolbar:
|
|
@@ -1332,6 +1466,103 @@ def render_workspace_controls(user):
|
|
| 1332 |
st.text_input(tr(lang_code, "topic_label"), value=DEFAULT_TOPIC, disabled=active, key="sp_topic")
|
| 1333 |
st.text_input(tr(lang_code, "keywords_label"), value=DEFAULT_KEYWORDS, disabled=active, key="sp_keywords")
|
| 1334 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1335 |
product_ui = get_product_ui_projection()
|
| 1336 |
if product_ui.get("mode") == "a5_product":
|
| 1337 |
products = product_ui.get("products") or {}
|
|
@@ -1386,51 +1617,47 @@ def render_workspace_controls(user):
|
|
| 1386 |
key="sp_words",
|
| 1387 |
)
|
| 1388 |
|
| 1389 |
-
|
| 1390 |
-
|
| 1391 |
-
st.
|
| 1392 |
-
|
| 1393 |
-
|
| 1394 |
-
|
| 1395 |
-
|
| 1396 |
-
|
| 1397 |
-
|
| 1398 |
-
|
| 1399 |
-
|
| 1400 |
-
|
| 1401 |
-
|
| 1402 |
-
|
| 1403 |
-
|
| 1404 |
-
|
| 1405 |
-
|
| 1406 |
-
|
| 1407 |
-
|
| 1408 |
-
|
| 1409 |
-
|
| 1410 |
-
|
| 1411 |
-
|
| 1412 |
-
|
| 1413 |
-
|
| 1414 |
-
|
| 1415 |
-
|
| 1416 |
-
|
| 1417 |
-
|
| 1418 |
-
|
| 1419 |
-
|
| 1420 |
-
|
| 1421 |
-
|
| 1422 |
-
|
| 1423 |
-
|
| 1424 |
-
|
| 1425 |
-
|
| 1426 |
-
st.
|
| 1427 |
-
|
| 1428 |
-
|
| 1429 |
-
|
| 1430 |
-
st.session_state["sp_scroll_to_generation_action"] = True
|
| 1431 |
-
st.info(tr(lang_code, "pipeline_start"))
|
| 1432 |
-
_inject_generation_action_scroll("amj")
|
| 1433 |
-
_select_active_task_tab(task, lang_code)
|
| 1434 |
inject_file_uploader_i18n(lang_code)
|
| 1435 |
|
| 1436 |
|
|
|
|
| 30 |
start_generation_task,
|
| 31 |
)
|
| 32 |
from i18n import (
|
| 33 |
+
LANGUAGE_OPTIONS, get_output_suffix, get_research_material_template,
|
| 34 |
+
get_target_flag, get_target_language, tr,
|
| 35 |
+
)
|
| 36 |
+
from core.advanced_method_registry import (
|
| 37 |
+
METHOD_FAMILIES, METHOD_RESOLVER_VERSION, method_capability,
|
| 38 |
+
resolve_generation_preflight,
|
| 39 |
)
|
| 40 |
from smartpaper_restore_bridge import edge_bridge
|
| 41 |
|
|
|
|
| 77 |
payload = {
|
| 78 |
"browse": tr(language, "browse_files"),
|
| 79 |
"uploaders": {
|
| 80 |
+
tr(language, "research_materials_label"): {
|
| 81 |
+
"title": tr(language, "drop_research_materials"),
|
| 82 |
+
"limit": tr(language, "limit_research_materials"),
|
| 83 |
+
},
|
| 84 |
+
# Legacy labels remain recognized for recovered historical UI state,
|
| 85 |
+
# but the unified workspace renders only research_materials_label.
|
| 86 |
tr(language, "upload_reference"): {
|
| 87 |
"title": tr(language, "drop_docs"),
|
| 88 |
"limit": tr(language, "limit_docs"),
|
|
|
|
| 304 |
"sp_last_ui_task_status": "",
|
| 305 |
"sp_scroll_to_generation_action": False,
|
| 306 |
"sp_completed_download_scroll_task_id": "",
|
| 307 |
+
"sp_method_confirmation": "",
|
| 308 |
+
"sp_method_confirmation_signature": "",
|
| 309 |
+
"sp_material_document_use": "reference",
|
| 310 |
}.items():
|
| 311 |
if key not in st.session_state:
|
| 312 |
st.session_state[key] = default
|
|
|
|
| 448 |
return "amj" if mode == "amj" else "asq"
|
| 449 |
|
| 450 |
|
| 451 |
+
def _task_method_family(task: dict) -> str:
|
| 452 |
+
"""Return frozen new-task method_family, with legacy resume fallback."""
|
| 453 |
+
task_id = str((task or {}).get("task_id") or "").strip()
|
| 454 |
+
if task_id:
|
| 455 |
+
request_path = _task_dir(task_id) / "request.json"
|
| 456 |
+
if request_path.is_file():
|
| 457 |
+
try:
|
| 458 |
+
payload = json.loads(request_path.read_text(encoding="utf-8"))
|
| 459 |
+
family = str(payload.get("method_family") or "").strip().casefold()
|
| 460 |
+
if family in METHOD_FAMILIES:
|
| 461 |
+
return family
|
| 462 |
+
except Exception:
|
| 463 |
+
pass
|
| 464 |
+
mode = _task_mode(task)
|
| 465 |
+
return "qualitative" if mode == "asq" else "survey_path"
|
| 466 |
+
|
| 467 |
+
|
| 468 |
def _task_output_path(task: dict) -> Path | None:
|
| 469 |
task_id = str(task.get("task_id") or "").strip()
|
| 470 |
filename = os.path.basename(str(task.get("output_filename") or "").strip())
|
|
|
|
| 492 |
return saved
|
| 493 |
|
| 494 |
|
| 495 |
+
def _uploaded_material_descriptors() -> list[dict[str, str]]:
|
| 496 |
+
out: list[dict[str, str]] = []
|
| 497 |
+
for item in st.session_state.get("up_research_materials") or []:
|
| 498 |
+
name = os.path.basename(str(getattr(item, "name", "") or ""))
|
| 499 |
+
mime_type = str(getattr(item, "type", "") or "")
|
| 500 |
+
if name or mime_type:
|
| 501 |
+
out.append({"name": name, "mime_type": mime_type})
|
| 502 |
+
return out
|
| 503 |
+
|
| 504 |
+
|
| 505 |
+
def _method_intent_signature() -> str:
|
| 506 |
+
payload = {
|
| 507 |
+
"topic": str(st.session_state.get("sp_topic") or DEFAULT_TOPIC),
|
| 508 |
+
"keywords": str(st.session_state.get("sp_keywords") or DEFAULT_KEYWORDS),
|
| 509 |
+
"materials": _uploaded_material_descriptors(),
|
| 510 |
+
}
|
| 511 |
+
return hashlib.sha256(
|
| 512 |
+
json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
| 513 |
+
).hexdigest()
|
| 514 |
+
|
| 515 |
+
|
| 516 |
+
def _current_method_preflight(*, confirmed_method_family: str | None = None) -> tuple[dict, str]:
|
| 517 |
+
signature = _method_intent_signature()
|
| 518 |
+
stored_confirmation = str(st.session_state.get("sp_method_confirmation") or "").strip()
|
| 519 |
+
stored_signature = str(st.session_state.get("sp_method_confirmation_signature") or "").strip()
|
| 520 |
+
if confirmed_method_family is None:
|
| 521 |
+
confirmed = stored_confirmation if stored_signature == signature else ""
|
| 522 |
+
else:
|
| 523 |
+
confirmed = str(confirmed_method_family or "").strip()
|
| 524 |
+
preflight = resolve_generation_preflight(
|
| 525 |
+
str(st.session_state.get("sp_topic") or DEFAULT_TOPIC),
|
| 526 |
+
str(st.session_state.get("sp_keywords") or DEFAULT_KEYWORDS),
|
| 527 |
+
materials=_uploaded_material_descriptors(),
|
| 528 |
+
confirmed_method_family=confirmed,
|
| 529 |
+
)
|
| 530 |
+
return preflight, signature
|
| 531 |
+
|
| 532 |
+
|
| 533 |
+
def _classify_and_save_research_materials(
|
| 534 |
+
method_family: str, inputs_dir: Path
|
| 535 |
+
) -> tuple[list[str], list[str], list[str], str]:
|
| 536 |
+
"""Map one visible uploader into legacy request buckets without changing engines."""
|
| 537 |
+
uploads = list(st.session_state.get("up_research_materials") or [])
|
| 538 |
+
if not uploads:
|
| 539 |
+
return [], [], [], "none"
|
| 540 |
+
|
| 541 |
+
structured = []
|
| 542 |
+
text_materials = []
|
| 543 |
+
documents = []
|
| 544 |
+
for item in uploads:
|
| 545 |
+
name = os.path.basename(str(getattr(item, "name", "") or ""))
|
| 546 |
+
ext = Path(name).suffix.lower()
|
| 547 |
+
if ext in {".csv", ".xls", ".xlsx"}:
|
| 548 |
+
structured.append(item)
|
| 549 |
+
elif ext == ".txt":
|
| 550 |
+
text_materials.append(item)
|
| 551 |
+
else:
|
| 552 |
+
documents.append(item)
|
| 553 |
+
|
| 554 |
+
decision = str(st.session_state.get("sp_material_document_use") or "reference").strip().lower()
|
| 555 |
+
if decision not in {"reference", "research"}:
|
| 556 |
+
decision = "reference"
|
| 557 |
+
|
| 558 |
+
reference_items = []
|
| 559 |
+
data_items = list(structured)
|
| 560 |
+
interview_items = []
|
| 561 |
+
|
| 562 |
+
if method_family == "qualitative":
|
| 563 |
+
interview_items.extend(text_materials)
|
| 564 |
+
if decision == "research":
|
| 565 |
+
interview_items.extend(documents)
|
| 566 |
+
else:
|
| 567 |
+
reference_items.extend(documents)
|
| 568 |
+
else:
|
| 569 |
+
# Structured spreadsheets are the only legacy-safe empirical-data path
|
| 570 |
+
# for current survey production. Text/doc materials remain reference
|
| 571 |
+
# context unless a dedicated future method adapter owns them.
|
| 572 |
+
reference_items.extend(text_materials)
|
| 573 |
+
reference_items.extend(documents)
|
| 574 |
+
|
| 575 |
+
refs = _save_uploaded(reference_items, inputs_dir, "ref")
|
| 576 |
+
data = _save_uploaded(data_items[:1], inputs_dir, "data")
|
| 577 |
+
interviews = _save_uploaded(interview_items[:1], inputs_dir, "interview")
|
| 578 |
+
return refs, data, interviews, decision
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
def _persist_task_request(
|
| 582 |
+
task: dict,
|
| 583 |
+
mode: str,
|
| 584 |
+
lang_code: str,
|
| 585 |
+
*,
|
| 586 |
+
method_family: str,
|
| 587 |
+
method_resolver_version: str,
|
| 588 |
+
) -> dict:
|
| 589 |
task_id = str(task.get("task_id") or "").strip()
|
| 590 |
numeric_id = (st.session_state.get("sp_user") or {}).get("numeric_id")
|
| 591 |
if not task_id or not isinstance(numeric_id, int) or numeric_id <= 0:
|
|
|
|
| 594 |
inputs_dir = task_dir / "inputs"
|
| 595 |
inputs_dir.mkdir(parents=True, exist_ok=True)
|
| 596 |
|
| 597 |
+
capability = method_capability(method_family)
|
| 598 |
+
expected_lane = str(capability.get("legacy_policy_lane") or "").strip().lower()
|
| 599 |
+
if expected_lane not in {"amj", "asq"} or expected_lane != str(mode or "").strip().lower():
|
| 600 |
+
raise RuntimeError("method_family_policy_lane_mismatch")
|
| 601 |
+
|
| 602 |
+
refs, data, interviews, material_use_decision = _classify_and_save_research_materials(
|
| 603 |
+
method_family, inputs_dir
|
| 604 |
+
)
|
| 605 |
|
| 606 |
request_data = {
|
| 607 |
"task_id": task_id,
|
| 608 |
"user_id": numeric_id,
|
| 609 |
"mode": mode,
|
| 610 |
+
"method_family": method_family,
|
| 611 |
+
"method_resolver_version": str(method_resolver_version or METHOD_RESOLVER_VERSION),
|
| 612 |
+
"derived_policy_lane": expected_lane,
|
| 613 |
+
"material_use_decision": material_use_decision,
|
| 614 |
"interface_language": lang_code,
|
| 615 |
"target_lang": get_target_language(lang_code),
|
| 616 |
"output_suffix": get_output_suffix(lang_code),
|
|
|
|
| 629 |
_atomic_json(task_dir / "request.json", request_data)
|
| 630 |
return request_data
|
| 631 |
|
|
|
|
| 632 |
def _pid_alive(pid: int) -> bool:
|
| 633 |
if not isinstance(pid, int) or pid <= 0:
|
| 634 |
return False
|
|
|
|
| 770 |
pending = st.session_state.get("sp_pending_launch")
|
| 771 |
if not isinstance(pending, dict):
|
| 772 |
return
|
| 773 |
+
|
| 774 |
+
pending_family = str(pending.get("method_family") or "").strip().casefold()
|
| 775 |
+
pending_mode = str(pending.get("mode") or "").strip().lower()
|
| 776 |
idempotency_key = str(pending.get("idempotency_key") or "").strip()
|
| 777 |
+
pending_signature = str(pending.get("method_intent_signature") or "").strip()
|
| 778 |
+
|
| 779 |
+
# U1-R2 repeats the capability preflight immediately before any task/quota
|
| 780 |
+
# creation so a stale rerun or modified session state cannot bypass it.
|
| 781 |
+
preflight, current_signature = _current_method_preflight(
|
| 782 |
+
confirmed_method_family=pending_family or None
|
| 783 |
+
)
|
| 784 |
+
resolved_family = str(preflight.get("method_family") or "").strip().casefold()
|
| 785 |
+
resolved_mode = str(preflight.get("legacy_policy_lane") or "").strip().lower()
|
| 786 |
+
if (
|
| 787 |
+
not bool(preflight.get("can_start_generation"))
|
| 788 |
+
or not pending_family
|
| 789 |
+
or pending_family != resolved_family
|
| 790 |
+
or pending_mode != resolved_mode
|
| 791 |
+
or not pending_signature
|
| 792 |
+
or pending_signature != current_signature
|
| 793 |
+
):
|
| 794 |
+
st.session_state["sp_pending_launch"] = None
|
| 795 |
+
reason = str(preflight.get("block_reason") or "method_preflight_changed")
|
| 796 |
+
if reason == "method_capability_blocked":
|
| 797 |
+
method_label = tr(lang_code, f"method_{resolved_family}")
|
| 798 |
+
st.warning(tr(lang_code, "method_not_supported", method=method_label))
|
| 799 |
+
else:
|
| 800 |
+
st.warning(tr(lang_code, "method_confirmation_required"))
|
| 801 |
+
return
|
| 802 |
+
|
| 803 |
projection = get_product_ui_projection()
|
| 804 |
selected_product_tier = (
|
| 805 |
str(st.session_state.get("sp_product_tier") or "").strip().upper()
|
|
|
|
| 809 |
try:
|
| 810 |
response = start_generation_task(
|
| 811 |
idempotency_key=idempotency_key,
|
| 812 |
+
mode=resolved_mode,
|
| 813 |
interface_language=lang_code,
|
| 814 |
target_words=int(st.session_state.get("sp_words") or APP_CONFIG["target_words_default"]),
|
| 815 |
product_tier=selected_product_tier,
|
|
|
|
| 838 |
return
|
| 839 |
st.session_state["sp_generation_task"] = task
|
| 840 |
st.session_state["sp_last_ui_task_status"] = str(task.get("status") or "")
|
| 841 |
+
st.session_state["run_mode"] = resolved_mode
|
| 842 |
try:
|
| 843 |
request_path = _task_dir(task_id) / "request.json"
|
| 844 |
if not request_path.is_file():
|
| 845 |
+
_persist_task_request(
|
| 846 |
+
task,
|
| 847 |
+
resolved_mode,
|
| 848 |
+
lang_code,
|
| 849 |
+
method_family=resolved_family,
|
| 850 |
+
method_resolver_version=str(
|
| 851 |
+
(preflight.get("resolution") or {}).get("resolver_version")
|
| 852 |
+
or METHOD_RESOLVER_VERSION
|
| 853 |
+
),
|
| 854 |
+
)
|
| 855 |
_launch_executor(task_id)
|
| 856 |
except Exception as exc:
|
| 857 |
st.session_state["sp_pending_launch"] = None
|
|
|
|
| 1355 |
return detail or code or tr(lang_code, "node_error"), ""
|
| 1356 |
|
| 1357 |
|
| 1358 |
+
def _inject_generation_action_scroll(mode: str | None = None) -> None:
|
| 1359 |
+
"""Scroll immediately to the unified generation action without a server wait."""
|
| 1360 |
+
target_id = "sp-generation-action"
|
| 1361 |
components.html(
|
| 1362 |
f"""
|
| 1363 |
<script>
|
|
|
|
| 1386 |
if not bool(st.session_state.get("sp_scroll_to_generation_action")):
|
| 1387 |
return
|
| 1388 |
st.session_state["sp_scroll_to_generation_action"] = False
|
| 1389 |
+
fallback_id = "sp-generation-action"
|
|
|
|
| 1390 |
components.html(
|
| 1391 |
f"""
|
| 1392 |
<script>
|
|
|
|
| 1416 |
)
|
| 1417 |
|
| 1418 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1419 |
def render_workspace_controls(user):
|
| 1420 |
top_toolbar = st.container()
|
| 1421 |
with top_toolbar:
|
|
|
|
| 1466 |
st.text_input(tr(lang_code, "topic_label"), value=DEFAULT_TOPIC, disabled=active, key="sp_topic")
|
| 1467 |
st.text_input(tr(lang_code, "keywords_label"), value=DEFAULT_KEYWORDS, disabled=active, key="sp_keywords")
|
| 1468 |
|
| 1469 |
+
# U1-R2 unified research workspace: academic intent/materials first, then
|
| 1470 |
+
# product/length controls. Legacy AMJ/ASQ remains internal policy routing.
|
| 1471 |
+
st.file_uploader(
|
| 1472 |
+
tr(lang_code, "research_materials_label"),
|
| 1473 |
+
type=["pdf", "txt", "doc", "docx", "csv", "xls", "xlsx"],
|
| 1474 |
+
accept_multiple_files=True,
|
| 1475 |
+
key="up_research_materials",
|
| 1476 |
+
disabled=active,
|
| 1477 |
+
help=tr(lang_code, "research_materials_help"),
|
| 1478 |
+
)
|
| 1479 |
+
|
| 1480 |
+
uploaded_materials = list(st.session_state.get("up_research_materials") or [])
|
| 1481 |
+
has_document_material = any(
|
| 1482 |
+
Path(str(getattr(item, "name", "") or "")).suffix.lower() in {".pdf", ".doc", ".docx"}
|
| 1483 |
+
for item in uploaded_materials
|
| 1484 |
+
)
|
| 1485 |
+
if has_document_material and not active:
|
| 1486 |
+
st.radio(
|
| 1487 |
+
tr(lang_code, "material_document_use_label"),
|
| 1488 |
+
["reference", "research"],
|
| 1489 |
+
format_func=lambda value: tr(lang_code, f"material_use_{value}"),
|
| 1490 |
+
horizontal=True,
|
| 1491 |
+
key="sp_material_document_use",
|
| 1492 |
+
)
|
| 1493 |
+
|
| 1494 |
+
if active:
|
| 1495 |
+
active_family = _task_method_family(task)
|
| 1496 |
+
active_capability = method_capability(active_family)
|
| 1497 |
+
preflight = {
|
| 1498 |
+
"can_start_generation": True,
|
| 1499 |
+
"block_reason": "",
|
| 1500 |
+
"method_family": active_family,
|
| 1501 |
+
"legacy_policy_lane": str(active_capability.get("legacy_policy_lane") or _task_mode(task)),
|
| 1502 |
+
"capability": active_capability,
|
| 1503 |
+
"resolution": {"confidence": "TASK_FROZEN", "resolver_version": METHOD_RESOLVER_VERSION},
|
| 1504 |
+
}
|
| 1505 |
+
method_signature = ""
|
| 1506 |
+
else:
|
| 1507 |
+
preflight, method_signature = _current_method_preflight()
|
| 1508 |
+
if str(preflight.get("block_reason") or "") == "method_confirmation_required":
|
| 1509 |
+
stored = str(st.session_state.get("sp_method_confirmation") or "").strip().casefold()
|
| 1510 |
+
stored_sig = str(st.session_state.get("sp_method_confirmation_signature") or "").strip()
|
| 1511 |
+
selected_default = stored if stored_sig == method_signature and stored in METHOD_FAMILIES else ""
|
| 1512 |
+
options = [""] + list(METHOD_FAMILIES)
|
| 1513 |
+
selected = st.selectbox(
|
| 1514 |
+
tr(lang_code, "method_confirm_label"),
|
| 1515 |
+
options,
|
| 1516 |
+
index=options.index(selected_default) if selected_default in options else 0,
|
| 1517 |
+
format_func=lambda family: (
|
| 1518 |
+
tr(lang_code, "method_confirm_placeholder")
|
| 1519 |
+
if not family else tr(lang_code, f"method_{family}")
|
| 1520 |
+
),
|
| 1521 |
+
)
|
| 1522 |
+
if selected:
|
| 1523 |
+
st.session_state["sp_method_confirmation"] = selected
|
| 1524 |
+
st.session_state["sp_method_confirmation_signature"] = method_signature
|
| 1525 |
+
preflight, method_signature = _current_method_preflight(confirmed_method_family=selected)
|
| 1526 |
+
else:
|
| 1527 |
+
# Explicit Title/Keyword cues are authoritative; stale confirmations
|
| 1528 |
+
# from a previous ambiguous intent cannot override them.
|
| 1529 |
+
st.session_state["sp_method_confirmation"] = ""
|
| 1530 |
+
st.session_state["sp_method_confirmation_signature"] = ""
|
| 1531 |
+
|
| 1532 |
+
method_family = str(preflight.get("method_family") or "undetermined").strip().casefold()
|
| 1533 |
+
method_label = (
|
| 1534 |
+
tr(lang_code, f"method_{method_family}")
|
| 1535 |
+
if method_family in METHOD_FAMILIES
|
| 1536 |
+
else tr(lang_code, "method_undetermined")
|
| 1537 |
+
)
|
| 1538 |
+
st.markdown(f"**{tr(lang_code, 'detected_method_label')}:** {method_label}")
|
| 1539 |
+
st.caption(tr(lang_code, "method_detection_explanation"))
|
| 1540 |
+
|
| 1541 |
+
block_reason = str(preflight.get("block_reason") or "")
|
| 1542 |
+
if block_reason == "method_capability_blocked":
|
| 1543 |
+
st.warning(tr(lang_code, "method_not_supported", method=method_label))
|
| 1544 |
+
elif block_reason == "method_confirmation_required":
|
| 1545 |
+
st.warning(tr(lang_code, "method_confirmation_required"))
|
| 1546 |
+
elif not active:
|
| 1547 |
+
st.info(tr(lang_code, "method_supported", method=method_label))
|
| 1548 |
+
|
| 1549 |
+
with st.expander(tr(lang_code, "adaptive_template_expander")):
|
| 1550 |
+
if method_family in METHOD_FAMILIES:
|
| 1551 |
+
st.markdown(tr(lang_code, f"template_help_{method_family}"))
|
| 1552 |
+
template_info = get_research_material_template(lang_code, method_family)
|
| 1553 |
+
template_data = str(template_info.get("content") or "")
|
| 1554 |
+
if template_data:
|
| 1555 |
+
st.code(template_data, language=str(template_info.get("code_language") or "text"))
|
| 1556 |
+
st.download_button(
|
| 1557 |
+
label=tr(lang_code, "download_research_template"),
|
| 1558 |
+
data=template_data,
|
| 1559 |
+
file_name=str(template_info.get("filename") or "SmartPaper_Research_Materials_Template.txt"),
|
| 1560 |
+
mime=str(template_info.get("mime_type") or "text/plain"),
|
| 1561 |
+
use_container_width=True,
|
| 1562 |
+
)
|
| 1563 |
+
else:
|
| 1564 |
+
st.markdown(tr(lang_code, "template_help_undetermined"))
|
| 1565 |
+
|
| 1566 |
product_ui = get_product_ui_projection()
|
| 1567 |
if product_ui.get("mode") == "a5_product":
|
| 1568 |
products = product_ui.get("products") or {}
|
|
|
|
| 1617 |
key="sp_words",
|
| 1618 |
)
|
| 1619 |
|
| 1620 |
+
st.markdown("<div id='sp-generation-action'></div>", unsafe_allow_html=True)
|
| 1621 |
+
if active:
|
| 1622 |
+
if st.button(tr(lang_code, "cancel_generation"), key="cancel_generation_unified"):
|
| 1623 |
+
st.session_state["sp_pending_cancel_task_id"] = str(task.get("task_id") or "")
|
| 1624 |
+
st.rerun()
|
| 1625 |
+
else:
|
| 1626 |
+
can_start = bool(preflight.get("can_start_generation"))
|
| 1627 |
+
if st.button(
|
| 1628 |
+
tr(lang_code, "start_generation_unified"),
|
| 1629 |
+
disabled=not can_start,
|
| 1630 |
+
key="btn_generation_unified",
|
| 1631 |
+
):
|
| 1632 |
+
# Re-resolve at click time; _process_pending_launch repeats this gate
|
| 1633 |
+
# again immediately before authoritative task/quota creation.
|
| 1634 |
+
click_preflight, click_signature = _current_method_preflight(
|
| 1635 |
+
confirmed_method_family=method_family if method_family in METHOD_FAMILIES else None
|
| 1636 |
+
)
|
| 1637 |
+
if not bool(click_preflight.get("can_start_generation")):
|
| 1638 |
+
reason = str(click_preflight.get("block_reason") or "")
|
| 1639 |
+
if reason == "method_capability_blocked":
|
| 1640 |
+
st.warning(tr(lang_code, "method_not_supported", method=method_label))
|
| 1641 |
+
else:
|
| 1642 |
+
st.warning(tr(lang_code, "method_confirmation_required"))
|
| 1643 |
+
else:
|
| 1644 |
+
resolved_family = str(click_preflight.get("method_family") or "").strip().casefold()
|
| 1645 |
+
resolved_mode = str(click_preflight.get("legacy_policy_lane") or "").strip().lower()
|
| 1646 |
+
st.session_state["run_mode"] = resolved_mode
|
| 1647 |
+
st.session_state["sp_pending_launch"] = {
|
| 1648 |
+
"mode": resolved_mode,
|
| 1649 |
+
"method_family": resolved_family,
|
| 1650 |
+
"method_resolver_version": str(
|
| 1651 |
+
(click_preflight.get("resolution") or {}).get("resolver_version")
|
| 1652 |
+
or METHOD_RESOLVER_VERSION
|
| 1653 |
+
),
|
| 1654 |
+
"method_intent_signature": click_signature,
|
| 1655 |
+
"idempotency_key": _new_generation_idempotency_key(),
|
| 1656 |
+
}
|
| 1657 |
+
st.session_state["sp_scroll_to_generation_action"] = True
|
| 1658 |
+
st.info(tr(lang_code, "pipeline_start"))
|
| 1659 |
+
_inject_generation_action_scroll()
|
| 1660 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1661 |
inject_file_uploader_i18n(lang_code)
|
| 1662 |
|
| 1663 |
|
core/advanced_method_registry.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""Deterministic method-family resolver and capability preflight for SmartPaper.
|
| 3 |
+
|
| 4 |
+
U1-R2 derives an academic ``method_family`` from frozen user intent
|
| 5 |
+
(Title/Keywords plus light material metadata), maps that family to the legacy
|
| 6 |
+
AMJ/ASQ policy lane, and blocks unsupported method families before task/quota
|
| 7 |
+
creation. It never selects providers/models and never rewrites Title/Keywords.
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
import re
|
| 13 |
+
from typing import Any, Iterable
|
| 14 |
+
|
| 15 |
+
METHOD_RESOLVER_VERSION = "smartpaper-auto-method-resolver-v2"
|
| 16 |
+
METHOD_CAPABILITY_REGISTRY_VERSION = "smartpaper-method-capability-registry-v2"
|
| 17 |
+
|
| 18 |
+
METHOD_FAMILIES = (
|
| 19 |
+
"survey_path",
|
| 20 |
+
"qualitative",
|
| 21 |
+
"fsqca",
|
| 22 |
+
"meta_analysis",
|
| 23 |
+
"panel_econometrics",
|
| 24 |
+
"bibliometric_integrative_review",
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# Capability status is executable U1-R2 preflight authority. Advanced methods
|
| 28 |
+
# remain BLOCKED until their dedicated engines are implemented.
|
| 29 |
+
_METHOD_CAPABILITIES: dict[str, dict[str, Any]] = {
|
| 30 |
+
"survey_path": {
|
| 31 |
+
"legacy_policy_lane": "amj",
|
| 32 |
+
"capability_status": "production_supported",
|
| 33 |
+
"evidence_timing": "pre_literature",
|
| 34 |
+
"engine_family": "quantitative_engine",
|
| 35 |
+
},
|
| 36 |
+
"qualitative": {
|
| 37 |
+
"legacy_policy_lane": "asq",
|
| 38 |
+
"capability_status": "production_supported",
|
| 39 |
+
"evidence_timing": "pre_literature",
|
| 40 |
+
"engine_family": "qualitative_engine",
|
| 41 |
+
},
|
| 42 |
+
"fsqca": {
|
| 43 |
+
"legacy_policy_lane": "amj",
|
| 44 |
+
"capability_status": "blocked_pending_engine",
|
| 45 |
+
"evidence_timing": "pre_literature",
|
| 46 |
+
"engine_family": "fsqca_engine_pending",
|
| 47 |
+
},
|
| 48 |
+
"meta_analysis": {
|
| 49 |
+
"legacy_policy_lane": "amj",
|
| 50 |
+
"capability_status": "blocked_pending_engine",
|
| 51 |
+
"evidence_timing": "post_literature",
|
| 52 |
+
"engine_family": "meta_analysis_engine_pending",
|
| 53 |
+
},
|
| 54 |
+
"panel_econometrics": {
|
| 55 |
+
"legacy_policy_lane": "amj",
|
| 56 |
+
"capability_status": "blocked_pending_engine",
|
| 57 |
+
"evidence_timing": "pre_literature",
|
| 58 |
+
"engine_family": "panel_econometrics_engine_pending",
|
| 59 |
+
},
|
| 60 |
+
"bibliometric_integrative_review": {
|
| 61 |
+
"legacy_policy_lane": "amj",
|
| 62 |
+
"capability_status": "blocked_pending_engine",
|
| 63 |
+
"evidence_timing": "post_literature",
|
| 64 |
+
"engine_family": "review_engine_pending",
|
| 65 |
+
},
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
# Priority matters. Method-defining paradigms outrank generic survey cues.
|
| 69 |
+
_METHOD_PATTERNS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
| 70 |
+
(
|
| 71 |
+
"fsqca",
|
| 72 |
+
(
|
| 73 |
+
r"\bfsqca\b", r"\bfuzzy[- ]set\s+qca\b", r"\bfuzzy[- ]set\s+qualitative\s+comparative\s+analysis\b",
|
| 74 |
+
r"\bconfigurational\s+(?:analysis|paths?|approach)\b", r"\btruth\s+table\b",
|
| 75 |
+
r"模糊集定性比较分析", r"定性比较分析", r"组态(?:分析|路径)",
|
| 76 |
+
r"нечетк\w*.{0,18}сравнительн\w*.{0,18}анализ", r"конфигурационн\w*.{0,12}анализ",
|
| 77 |
+
r"бұлыңғыр.{0,16}сапалық.{0,16}салыстырмалы.{0,16}талдау", r"конфигурациялық.{0,12}талдау",
|
| 78 |
+
),
|
| 79 |
+
),
|
| 80 |
+
(
|
| 81 |
+
"meta_analysis",
|
| 82 |
+
(
|
| 83 |
+
r"\bmeta[- ]analysis\b", r"\bmeta[- ]analytic\b", r"\bmeta analysis\b",
|
| 84 |
+
r"元分析", r"荟萃分析",
|
| 85 |
+
r"мета[- ]?анализ", r"метааналитическ",
|
| 86 |
+
r"мета[- ]?талдау", r"метааналитикалық",
|
| 87 |
+
),
|
| 88 |
+
),
|
| 89 |
+
(
|
| 90 |
+
"bibliometric_integrative_review",
|
| 91 |
+
(
|
| 92 |
+
r"\bbibliometric\b", r"\bscience\s+mapping\b", r"\bco[- ]citation\b", r"\bco[- ]word\b",
|
| 93 |
+
r"\bintegrative\s+(?:theory\s+)?review\b", r"\btheory\s+review\b", r"\bsystematic\s+literature\s+review\b",
|
| 94 |
+
r"文献计量", r"科学知识图谱", r"共被引", r"共词", r"整合性(?:理论)?综述", r"系统性文献综述",
|
| 95 |
+
r"библиометр", r"научн\w*.{0,8}картирован", r"коцитирован", r"интегративн\w*.{0,8}обзор",
|
| 96 |
+
r"библиометр", r"ғылыми.{0,8}карталау", r"бірлескен.{0,8}дәйексөз", r"интегративті.{0,8}шолу",
|
| 97 |
+
),
|
| 98 |
+
),
|
| 99 |
+
(
|
| 100 |
+
"panel_econometrics",
|
| 101 |
+
(
|
| 102 |
+
r"\bpanel\s+data\b", r"\bfirm[- ]year\b", r"\bentity\s+fixed\s+effects?\b", r"\bfixed\s+effects?\b",
|
| 103 |
+
r"\brandom\s+effects?\b", r"\bpanel\s+econometrics?\b", r"\btwo[- ]way\s+fixed\s+effects?\b",
|
| 104 |
+
r"面板数据", r"固定效应", r"双向固定效应", r"公司[-—– ]?年度",
|
| 105 |
+
r"панельн\w*.{0,8}данн", r"фиксированн\w*.{0,8}эффект",
|
| 106 |
+
r"панельдік.{0,8}дерек", r"тұрақты.{0,8}әсер",
|
| 107 |
+
),
|
| 108 |
+
),
|
| 109 |
+
(
|
| 110 |
+
"qualitative",
|
| 111 |
+
(
|
| 112 |
+
r"\bqualitative\s+(?:study|research|analysis)\b", r"\bgioia\b", r"\bgrounded\s+theory\b",
|
| 113 |
+
r"\bcase\s+study\b", r"\bcomparative\s+case\b", r"\bprocess\s+study\b", r"\bethnograph",
|
| 114 |
+
r"\bsemi[- ]structured\s+interviews?\b", r"\binterviews?\b",
|
| 115 |
+
r"定性研究", r"质性研究", r"扎根理论", r"案例研究", r"个案研究", r"访谈", r"民族志",
|
| 116 |
+
r"качественн\w*.{0,8}(?:исследован|анализ)", r"обоснованн\w*.{0,8}теори", r"интервью", r"кейс[- ]?исследован",
|
| 117 |
+
r"сапалық.{0,8}(?:зерттеу|талдау)", r"негізделген.{0,8}теори", r"сұхбат", r"кейс.{0,8}зерттеу",
|
| 118 |
+
),
|
| 119 |
+
),
|
| 120 |
+
(
|
| 121 |
+
"survey_path",
|
| 122 |
+
(
|
| 123 |
+
r"\bsurvey\b", r"\bquestionnaire\b", r"\bstructural\s+equation\s+model", r"\bsem\b", r"\bpls[- ]sem\b",
|
| 124 |
+
r"\bregression\b", r"\bmediating\s+role\b", r"\bmoderating\s+role\b", r"\bmediation\b", r"\bmoderation\b",
|
| 125 |
+
r"问卷", r"结构方程", r"回归", r"中介(?:作用|效应|变量)?", r"调节(?:作用|效应|变量)?",
|
| 126 |
+
r"опрос", r"анкет", r"структурн\w*.{0,10}уравнен", r"регресс", r"медиирующ", r"модерирующ",
|
| 127 |
+
r"сауалнама", r"құрылымдық.{0,10}теңдеу", r"регресс", r"медиаторлық", r"модераторлық",
|
| 128 |
+
),
|
| 129 |
+
),
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
_MATERIAL_NAME_PATTERNS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
| 133 |
+
("fsqca", (r"fsqca", r"truth[-_ ]?table", r"calibrat", r"condition[-_ ]?outcome")),
|
| 134 |
+
("meta_analysis", (r"meta[-_ ]?analysis", r"effect[-_ ]?size", r"forest[-_ ]?plot", r"study[-_ ]?character")),
|
| 135 |
+
("bibliometric_integrative_review", (r"bibliometric", r"corpus", r"co[-_ ]?citation", r"co[-_ ]?word", r"openalex")),
|
| 136 |
+
("panel_econometrics", (r"panel", r"firm[-_ ]?year", r"entity[-_ ]?year", r"fixed[-_ ]?effect")),
|
| 137 |
+
("qualitative", (r"interview", r"transcript", r"field[-_ ]?note", r"focus[-_ ]?group", r"访谈", r"сұхбат", r"интервью")),
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def _normalized_text(value: Any) -> str:
|
| 142 |
+
return re.sub(r"\s+", " ", str(value or "")).strip()
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def _keyword_items(value: Any) -> list[str]:
|
| 146 |
+
if isinstance(value, (list, tuple)):
|
| 147 |
+
raw = list(value)
|
| 148 |
+
else:
|
| 149 |
+
raw = re.split(r"[;\n|]+", str(value or ""))
|
| 150 |
+
if len(raw) == 1:
|
| 151 |
+
raw = re.split(r"[,]", str(value or ""))
|
| 152 |
+
return [_normalized_text(item) for item in raw if _normalized_text(item)]
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def _material_descriptors(materials: Iterable[Any] | None) -> list[dict[str, str]]:
|
| 156 |
+
out: list[dict[str, str]] = []
|
| 157 |
+
for item in materials or []:
|
| 158 |
+
if isinstance(item, dict):
|
| 159 |
+
name = _normalized_text(item.get("name") or item.get("filename"))
|
| 160 |
+
mime = _normalized_text(item.get("mime_type") or item.get("type"))
|
| 161 |
+
else:
|
| 162 |
+
name = _normalized_text(getattr(item, "name", ""))
|
| 163 |
+
mime = _normalized_text(getattr(item, "type", ""))
|
| 164 |
+
if not name and not mime:
|
| 165 |
+
continue
|
| 166 |
+
out.append({
|
| 167 |
+
"name": name,
|
| 168 |
+
"mime_type": mime,
|
| 169 |
+
"extension": os.path.splitext(name)[1].lower().lstrip("."),
|
| 170 |
+
})
|
| 171 |
+
return out
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def method_capability(method_family: str) -> dict[str, Any]:
|
| 175 |
+
"""Return a copy of versioned capability metadata for one method family."""
|
| 176 |
+
family = str(method_family or "").strip().casefold()
|
| 177 |
+
row = dict(_METHOD_CAPABILITIES.get(family) or {})
|
| 178 |
+
if not row:
|
| 179 |
+
return {
|
| 180 |
+
"registry_version": METHOD_CAPABILITY_REGISTRY_VERSION,
|
| 181 |
+
"method_family": "undetermined",
|
| 182 |
+
"legacy_policy_lane": None,
|
| 183 |
+
"capability_status": "ambiguous",
|
| 184 |
+
"evidence_timing": "undetermined",
|
| 185 |
+
"engine_family": "undetermined",
|
| 186 |
+
}
|
| 187 |
+
row.update({
|
| 188 |
+
"registry_version": METHOD_CAPABILITY_REGISTRY_VERSION,
|
| 189 |
+
"method_family": family,
|
| 190 |
+
})
|
| 191 |
+
return row
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def resolve_method_family(
|
| 195 |
+
title: Any,
|
| 196 |
+
keywords: Any,
|
| 197 |
+
*,
|
| 198 |
+
materials: Iterable[Any] | None = None,
|
| 199 |
+
) -> dict[str, Any]:
|
| 200 |
+
"""Resolve an academic method family from frozen user intent.
|
| 201 |
+
|
| 202 |
+
Explicit Title/Keyword cues are authoritative. Material *metadata* can add
|
| 203 |
+
support, but never overrides an explicit method cue. File contents are
|
| 204 |
+
deliberately not read here.
|
| 205 |
+
"""
|
| 206 |
+
source_title = _normalized_text(title)
|
| 207 |
+
source_keywords = _keyword_items(keywords)
|
| 208 |
+
text = " ; ".join([source_title] + source_keywords)
|
| 209 |
+
|
| 210 |
+
hits: list[dict[str, Any]] = []
|
| 211 |
+
chosen = ""
|
| 212 |
+
for family, patterns in _METHOD_PATTERNS:
|
| 213 |
+
matched = [pattern for pattern in patterns if re.search(pattern, text, flags=re.I | re.UNICODE)]
|
| 214 |
+
if matched:
|
| 215 |
+
hits.append({"method_family": family, "source": "title_keywords", "match_count": len(matched)})
|
| 216 |
+
if not chosen:
|
| 217 |
+
chosen = family
|
| 218 |
+
|
| 219 |
+
descriptors = _material_descriptors(materials)
|
| 220 |
+
material_hits: list[str] = []
|
| 221 |
+
for descriptor in descriptors:
|
| 222 |
+
name = descriptor.get("name") or ""
|
| 223 |
+
for family, patterns in _MATERIAL_NAME_PATTERNS:
|
| 224 |
+
if any(re.search(pattern, name, flags=re.I | re.UNICODE) for pattern in patterns):
|
| 225 |
+
if family not in material_hits:
|
| 226 |
+
material_hits.append(family)
|
| 227 |
+
|
| 228 |
+
# Explicit academic wording outranks material filename hints. When there is
|
| 229 |
+
# no explicit cue, one unique material hint is useful but remains ambiguous
|
| 230 |
+
# until future U1 benchmarks justify auto-acceptance.
|
| 231 |
+
confidence = "HIGH_CONFIDENCE" if chosen else "AMBIGUOUS"
|
| 232 |
+
reason_codes: list[str] = []
|
| 233 |
+
if chosen:
|
| 234 |
+
reason_codes.append("explicit_title_keyword_method_cue")
|
| 235 |
+
conflicting = [row["method_family"] for row in hits if row["method_family"] != chosen]
|
| 236 |
+
if conflicting:
|
| 237 |
+
reason_codes.append("lower_priority_method_cues_present")
|
| 238 |
+
elif len(material_hits) == 1:
|
| 239 |
+
chosen = material_hits[0]
|
| 240 |
+
reason_codes.append("material_filename_method_hint_only")
|
| 241 |
+
elif len(material_hits) > 1:
|
| 242 |
+
reason_codes.append("conflicting_material_method_hints")
|
| 243 |
+
else:
|
| 244 |
+
reason_codes.append("no_explicit_method_cue")
|
| 245 |
+
|
| 246 |
+
capability = method_capability(chosen)
|
| 247 |
+
return {
|
| 248 |
+
"resolver_version": METHOD_RESOLVER_VERSION,
|
| 249 |
+
"shadow_only": False,
|
| 250 |
+
"method_family": chosen or "undetermined",
|
| 251 |
+
"confidence": confidence,
|
| 252 |
+
"reason_codes": reason_codes,
|
| 253 |
+
"title_keyword_hits": hits,
|
| 254 |
+
"material_hint_families": material_hits,
|
| 255 |
+
"material_count": len(descriptors),
|
| 256 |
+
"derived_policy_lane": capability.get("legacy_policy_lane"),
|
| 257 |
+
"capability": capability,
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def compare_shadow_to_legacy_lane(resolution: dict[str, Any], selected_lane: Any) -> dict[str, Any]:
|
| 262 |
+
"""Compare shadow inference with the legacy user-selected AMJ/ASQ lane."""
|
| 263 |
+
selected = str(selected_lane or "").strip().casefold()
|
| 264 |
+
derived = str((resolution or {}).get("derived_policy_lane") or "").strip().casefold()
|
| 265 |
+
return {
|
| 266 |
+
"selected_legacy_lane": selected or None,
|
| 267 |
+
"derived_policy_lane": derived or None,
|
| 268 |
+
"lane_match": bool(selected and derived and selected == derived),
|
| 269 |
+
"comparable": bool(selected and derived),
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def resolve_generation_preflight(
|
| 274 |
+
title: Any,
|
| 275 |
+
keywords: Any,
|
| 276 |
+
*,
|
| 277 |
+
materials: Iterable[Any] | None = None,
|
| 278 |
+
confirmed_method_family: Any = None,
|
| 279 |
+
) -> dict[str, Any]:
|
| 280 |
+
"""Resolve method + capability before any task/quota creation.
|
| 281 |
+
|
| 282 |
+
A user confirmation is accepted only when deterministic resolution is
|
| 283 |
+
genuinely ambiguous. Explicit Title/Keyword cues always remain authoritative.
|
| 284 |
+
"""
|
| 285 |
+
resolution = resolve_method_family(title, keywords, materials=materials)
|
| 286 |
+
family = str(resolution.get("method_family") or "").strip().casefold()
|
| 287 |
+
confidence = str(resolution.get("confidence") or "").strip().upper()
|
| 288 |
+
|
| 289 |
+
confirmed = str(confirmed_method_family or "").strip().casefold()
|
| 290 |
+
if confidence == "AMBIGUOUS":
|
| 291 |
+
if confirmed in METHOD_FAMILIES:
|
| 292 |
+
family = confirmed
|
| 293 |
+
capability = method_capability(family)
|
| 294 |
+
resolution = dict(resolution)
|
| 295 |
+
resolution.update({
|
| 296 |
+
"method_family": family,
|
| 297 |
+
"confidence": "USER_CONFIRMED",
|
| 298 |
+
"derived_policy_lane": capability.get("legacy_policy_lane"),
|
| 299 |
+
"capability": capability,
|
| 300 |
+
"reason_codes": list(resolution.get("reason_codes") or []) + ["user_confirmed_method_family"],
|
| 301 |
+
})
|
| 302 |
+
else:
|
| 303 |
+
return {
|
| 304 |
+
"can_start_generation": False,
|
| 305 |
+
"block_reason": "method_confirmation_required",
|
| 306 |
+
"resolution": resolution,
|
| 307 |
+
"method_family": family or "undetermined",
|
| 308 |
+
"legacy_policy_lane": None,
|
| 309 |
+
"capability": method_capability(family),
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
capability = method_capability(family)
|
| 313 |
+
lane = str(capability.get("legacy_policy_lane") or "").strip().casefold() or None
|
| 314 |
+
status = str(capability.get("capability_status") or "").strip().casefold()
|
| 315 |
+
if family not in METHOD_FAMILIES or not lane:
|
| 316 |
+
return {
|
| 317 |
+
"can_start_generation": False,
|
| 318 |
+
"block_reason": "method_confirmation_required",
|
| 319 |
+
"resolution": resolution,
|
| 320 |
+
"method_family": family or "undetermined",
|
| 321 |
+
"legacy_policy_lane": None,
|
| 322 |
+
"capability": capability,
|
| 323 |
+
}
|
| 324 |
+
if status != "production_supported":
|
| 325 |
+
return {
|
| 326 |
+
"can_start_generation": False,
|
| 327 |
+
"block_reason": "method_capability_blocked",
|
| 328 |
+
"resolution": resolution,
|
| 329 |
+
"method_family": family,
|
| 330 |
+
"legacy_policy_lane": lane,
|
| 331 |
+
"capability": capability,
|
| 332 |
+
}
|
| 333 |
+
return {
|
| 334 |
+
"can_start_generation": True,
|
| 335 |
+
"block_reason": "",
|
| 336 |
+
"resolution": resolution,
|
| 337 |
+
"method_family": family,
|
| 338 |
+
"legacy_policy_lane": lane,
|
| 339 |
+
"capability": capability,
|
| 340 |
+
}
|
i18n.py
CHANGED
|
@@ -206,10 +206,10 @@ REFERENCE_HEADINGS = {
|
|
| 206 |
TEXTS: dict[str, dict[str, str]] = {
|
| 207 |
"zh": {
|
| 208 |
"title": "📊 工商管理论文智能体",
|
| 209 |
-
"topic_label": "📝 论文
|
| 210 |
-
"keywords_label": "🔍 关键
|
| 211 |
-
"words_label": "📏 目标
|
| 212 |
-
"words_label_product": "📏 目标正文
|
| 213 |
"product_tier_label": "版本",
|
| 214 |
"product_tier_standard": "标准版",
|
| 215 |
"product_tier_flagship": "旗舰版",
|
|
@@ -275,6 +275,37 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 275 |
"asq_hint": "**⚠️ 核心提示**:为了让AI准确提取语录和编码树,请务必标注**受访者角色(Role)**,并采用清晰的**对话格式**。",
|
| 276 |
"download_asq_template": "📥 一键下载 TXT 访谈模板",
|
| 277 |
"start_asq": "🚀 启动 ASQ 模式生成",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
"api_key_missing": "❌ 缺少 DEEPSEEK_API_KEY,请配置API密钥。",
|
| 279 |
"resume_toast": "🔄 检测到页面刷新或网络抖动,系统正在自动恢复进度……",
|
| 280 |
"pipeline_start": "⏳ 流水线启动……",
|
|
@@ -416,10 +447,10 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 416 |
},
|
| 417 |
"en": {
|
| 418 |
"title": "📊 Business Administration Article Agent",
|
| 419 |
-
"topic_label": "📝
|
| 420 |
-
"keywords_label": "🔍 Keywords",
|
| 421 |
-
"words_label": "📏 Target Word Count ({min_words}–{max_words} words)",
|
| 422 |
-
"words_label_product": "📏 Target
|
| 423 |
"product_tier_label": "Version",
|
| 424 |
"product_tier_standard": "Standard",
|
| 425 |
"product_tier_flagship": "Flagship",
|
|
@@ -433,7 +464,7 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 433 |
"resume_unavailable_runtime_replaced": "⚠️ A paused unfinished paper task was detected, but this runtime no longer has the local request or checkpoint files required to continue it. Resume has been disabled to prevent an unsafe recovery. To release this task and start again, explicitly choose ‘Delete and Start Again’.",
|
| 434 |
"resume": "🔄 Continue Previous Task",
|
| 435 |
"clear": "🗑️ Delete and Start Again",
|
| 436 |
-
"cancel_generation": "⛔ Cancel
|
| 437 |
"draft_preparing": "Preparing the English draft…",
|
| 438 |
"draft_stage_inputs": "Organizing research inputs and evidence…",
|
| 439 |
"draft_stage_search": "Searching the academic literature…",
|
|
@@ -485,6 +516,37 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 485 |
"asq_hint": "**⚠️ Important**: Clearly label each **informant role** and use a consistent **dialogue format** so the system can accurately extract quotations and build the coding structure.",
|
| 486 |
"download_asq_template": "📥 Download TXT Interview Template",
|
| 487 |
"start_asq": "🚀 Start ASQ Generation",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
"api_key_missing": "❌ DEEPSEEK_API_KEY is missing. Configure the API key.",
|
| 489 |
"resume_toast": "🔄 A page refresh or network interruption was detected. Progress is being restored automatically…",
|
| 490 |
"pipeline_start": "⏳ Starting the pipeline…",
|
|
@@ -626,10 +688,10 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 626 |
},
|
| 627 |
"ru": {
|
| 628 |
"title": "📊 Интеллектуальный агент для научных статей по управлению бизнесом",
|
| 629 |
-
"topic_label": "📝 Название статьи",
|
| 630 |
-
"keywords_label": "🔍 Ключевые слова",
|
| 631 |
-
"words_label": "📏 Целевой объём ({min_words}–{max_words} слов)",
|
| 632 |
-
"words_label_product": "📏 Целевой объём
|
| 633 |
"product_tier_label": "Версия",
|
| 634 |
"product_tier_standard": "Стандарт",
|
| 635 |
"product_tier_flagship": "Флагман",
|
|
@@ -643,7 +705,7 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 643 |
"resume_unavailable_runtime_replaced": "⚠️ Обнаружена приостановленная незавершённая задача, но в текущей среде выполнения отсутствуют локальный файл запроса или контрольная точка, необходимые для продолжения. Чтобы избежать некорректного восстановления, кнопка продолжения отключена. Для освобождения задачи и нового запуска явно выберите «Удалить и начать заново».",
|
| 644 |
"resume": "🔄 Продолжить предыдущую задачу",
|
| 645 |
"clear": "🗑️ Удалить и начать заново",
|
| 646 |
-
"cancel_generation": "⛔ Отменить
|
| 647 |
"draft_preparing": "Подготовка черновика на английском языке…",
|
| 648 |
"draft_stage_inputs": "Систематизация исследовательских данных и доказательств…",
|
| 649 |
"draft_stage_search": "Поиск научной литературы…",
|
|
@@ -695,6 +757,37 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 695 |
"asq_hint": "**⚠️ Важно**: укажите **роль информанта** и соблюдайте единый **формат диалога**, чтобы система точно извлекала цитаты и строила структуру кодирования.",
|
| 696 |
"download_asq_template": "📥 Скачать TXT-шаблон интервью",
|
| 697 |
"start_asq": "🚀 Запустить формирование в режиме ASQ",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 698 |
"api_key_missing": "❌ Не задан DEEPSEEK_API_KEY. Настройте ключ API.",
|
| 699 |
"resume_toast": "🔄 Обнаружено обновление страницы или нарушение соединения. Выполняется автоматическое восстановление…",
|
| 700 |
"pipeline_start": "⏳ Запуск конвейера…",
|
|
@@ -836,10 +929,10 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 836 |
},
|
| 837 |
"kk": {
|
| 838 |
"title": "📊 Бизнес-басқару жөніндегі ғылыми мақала интеллектуалды агенті",
|
| 839 |
-
"topic_label": "📝 Мақала
|
| 840 |
-
"keywords_label": "🔍
|
| 841 |
-
"words_label": "📏 Мақсатты
|
| 842 |
-
"words_label_product": "📏
|
| 843 |
"product_tier_label": "Нұсқа",
|
| 844 |
"product_tier_standard": "Стандарт",
|
| 845 |
"product_tier_flagship": "Флагман",
|
|
@@ -853,7 +946,7 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 853 |
"resume_unavailable_runtime_replaced": "⚠️ Уақытша тоқтатылған аяқталмаған тапсырма анықталды, бірақ ағымдағы іске қосу ортасында оны жалғастыруға қажет жергілікті сұрау немесе бақылау нүктесі файлдары жоқ. Қате қалпына келтіруге жол бермеу үшін жалғастыру түймесі өшірілді. Тапс��рманы босатып, қайта бастау үшін «Өшіріп, қайта бастау» әрекетін нақты таңдаңыз.",
|
| 854 |
"resume": "🔄 Алдыңғы тапсырманы жалғастыру",
|
| 855 |
"clear": "🗑️ Өшіріп, қайта бастау",
|
| 856 |
-
"cancel_generation": "⛔
|
| 857 |
"draft_preparing": "Ағылшын тіліндегі бастапқы нұсқа дайындалуда…",
|
| 858 |
"draft_stage_inputs": "Зерттеу кірістері мен дәлелдер реттелуде…",
|
| 859 |
"draft_stage_search": "Ғылыми әдебиеттер ізделуде…",
|
|
@@ -905,6 +998,37 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 905 |
"asq_hint": "**⚠️ Маңызды**: жүйе дәйексөздерді дәл шығарып, кодтау құрылымын құруы үшін **ақпарат берушінің рөлін** көрсетіп, бірізді **диалог пішімін** қолданыңыз.",
|
| 906 |
"download_asq_template": "📥 TXT сұхбат үлгісін жүктеу",
|
| 907 |
"start_asq": "🚀 ASQ режимінде дайындауды бастау",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 908 |
"api_key_missing": "❌ DEEPSEEK_API_KEY көрсетілмеген. API кілтін баптаңыз.",
|
| 909 |
"resume_toast": "🔄 Бет жаңартылғаны немесе желі үзілісі анықталды. Үдеріс автоматты түрде қалпына келтірілуде…",
|
| 910 |
"pipeline_start": "⏳ Жұмыс үдерісі басталуда…",
|
|
@@ -1080,5 +1204,53 @@ def get_asq_txt_template(language: str) -> str:
|
|
| 1080 |
return ASQ_TXT_TEMPLATES[normalize_language(language)]
|
| 1081 |
|
| 1082 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1083 |
def get_reference_heading(language: str) -> str:
|
| 1084 |
return REFERENCE_HEADINGS[normalize_language(language)]
|
|
|
|
| 206 |
TEXTS: dict[str, dict[str, str]] = {
|
| 207 |
"zh": {
|
| 208 |
"title": "📊 工商管理论文智能体",
|
| 209 |
+
"topic_label": "📝 论文题目(必填)",
|
| 210 |
+
"keywords_label": "🔍 关键词(必填)",
|
| 211 |
+
"words_label": "📏 目标正文字数(必填,{min_words}–{max_words} 单词)",
|
| 212 |
+
"words_label_product": "📏 目标正文字数(必填,{min_words}–{max_words} 单词)",
|
| 213 |
"product_tier_label": "版本",
|
| 214 |
"product_tier_standard": "标准版",
|
| 215 |
"product_tier_flagship": "旗舰版",
|
|
|
|
| 275 |
"asq_hint": "**⚠️ 核心提示**:为了让AI准确提取语录和编码树,请务必标注**受访者角色(Role)**,并采用清晰的**对话格式**。",
|
| 276 |
"download_asq_template": "📥 一键下载 TXT 访谈模板",
|
| 277 |
"start_asq": "🚀 启动 ASQ 模式生成",
|
| 278 |
+
"research_materials_label": "研究资料(可选)",
|
| 279 |
+
"research_materials_help": "支持 PDF、TXT、DOC、DOCX、CSV、XLS、XLSX。SmartPaper 会结合论文题目、关键词与文件特征判断研究设计和资料用途。",
|
| 280 |
+
"drop_research_materials": "将研究资料拖到这里",
|
| 281 |
+
"limit_research_materials": "每个文件最大 1 GB · PDF, TXT, DOC, DOCX, CSV, XLS, XLSX",
|
| 282 |
+
"material_document_use_label": "如何使用上传的 PDF/DOC/DOCX 文件?",
|
| 283 |
+
"material_use_reference": "作为参考文献资料",
|
| 284 |
+
"material_use_research": "作为研究资料",
|
| 285 |
+
"detected_method_label": "研究设计(自动识别)",
|
| 286 |
+
"method_detection_explanation": "SmartPaper 根据论文题目、关键词和可选研究资料自动识别研究方法;不会修改你的题目或关键词。",
|
| 287 |
+
"method_confirm_label": "请确认研究设计",
|
| 288 |
+
"method_confirm_placeholder": "请选择研究方法",
|
| 289 |
+
"method_confirmation_required": "当前信息不足以可靠识别研究设计,请先确认研究方法。",
|
| 290 |
+
"method_not_supported": "已识别为“{method}”。该研究方法的专用生成引擎尚未开放;本次不会创建任务,也不会占用额度。",
|
| 291 |
+
"method_supported": "已识别为“{method}”。系统将按对应研究流程生成论文。",
|
| 292 |
+
"method_undetermined": "尚未确定",
|
| 293 |
+
"method_survey_path": "问卷与路径关系研究",
|
| 294 |
+
"method_qualitative": "定性研究",
|
| 295 |
+
"method_fsqca": "fsQCA 组态研究",
|
| 296 |
+
"method_meta_analysis": "元分析",
|
| 297 |
+
"method_panel_econometrics": "面板计量研究",
|
| 298 |
+
"method_bibliometric_integrative_review": "文献计量与整合性综述",
|
| 299 |
+
"adaptive_template_expander": "💡 推荐模板(可选)",
|
| 300 |
+
"download_research_template": "📥 下载研究资料模板",
|
| 301 |
+
"template_help_survey_path": "推荐使用变量或统计结果表。若未上传数据,系统会按项目规则生成用于初稿的辅助证据。",
|
| 302 |
+
"template_help_qualitative": "推荐使用访谈、田野记录或案例材料。若未上传资料,系统会构建匿名的初稿证据包。",
|
| 303 |
+
"template_help_fsqca": "推荐使用条件变量、结果变量及校准信息表。专用 fsQCA 引擎开放后将按此结构处理。",
|
| 304 |
+
"template_help_meta_analysis": "推荐使用研究层面的效应量提取表。真实论文身份与系统辅助效应量必须严格分离。",
|
| 305 |
+
"template_help_panel_econometrics": "推荐使用实体—时间面板数据表,例如企业—年度结构。",
|
| 306 |
+
"template_help_bibliometric_integrative_review": "推荐使用真实文献语料的题名、作者、年份、DOI、期刊和关键词等元数据。",
|
| 307 |
+
"template_help_undetermined": "确认研究设计后,系统将显示对应的研究资料模板。",
|
| 308 |
+
"start_generation_unified": "🚀 开始生成",
|
| 309 |
"api_key_missing": "❌ 缺少 DEEPSEEK_API_KEY,请配置API密钥。",
|
| 310 |
"resume_toast": "🔄 检测到页面刷新或网络抖动,系统正在自动恢复进度……",
|
| 311 |
"pipeline_start": "⏳ 流水线启动……",
|
|
|
|
| 447 |
},
|
| 448 |
"en": {
|
| 449 |
"title": "📊 Business Administration Article Agent",
|
| 450 |
+
"topic_label": "📝 Title (Required)",
|
| 451 |
+
"keywords_label": "🔍 Keywords (Required)",
|
| 452 |
+
"words_label": "📏 Target Word Count (Required, {min_words}–{max_words} words)",
|
| 453 |
+
"words_label_product": "📏 Target Word Count (Required, {min_words}–{max_words} words)",
|
| 454 |
"product_tier_label": "Version",
|
| 455 |
"product_tier_standard": "Standard",
|
| 456 |
"product_tier_flagship": "Flagship",
|
|
|
|
| 464 |
"resume_unavailable_runtime_replaced": "⚠️ A paused unfinished paper task was detected, but this runtime no longer has the local request or checkpoint files required to continue it. Resume has been disabled to prevent an unsafe recovery. To release this task and start again, explicitly choose ‘Delete and Start Again’.",
|
| 465 |
"resume": "🔄 Continue Previous Task",
|
| 466 |
"clear": "🗑️ Delete and Start Again",
|
| 467 |
+
"cancel_generation": "⛔ Cancel",
|
| 468 |
"draft_preparing": "Preparing the English draft…",
|
| 469 |
"draft_stage_inputs": "Organizing research inputs and evidence…",
|
| 470 |
"draft_stage_search": "Searching the academic literature…",
|
|
|
|
| 516 |
"asq_hint": "**⚠️ Important**: Clearly label each **informant role** and use a consistent **dialogue format** so the system can accurately extract quotations and build the coding structure.",
|
| 517 |
"download_asq_template": "📥 Download TXT Interview Template",
|
| 518 |
"start_asq": "🚀 Start ASQ Generation",
|
| 519 |
+
"research_materials_label": "Research Materials (Optional)",
|
| 520 |
+
"research_materials_help": "Supports PDF, TXT, DOC, DOCX, CSV, XLS, and XLSX. SmartPaper uses the Title, Keywords, and file characteristics to infer the research design and material role.",
|
| 521 |
+
"drop_research_materials": "Drag research materials here",
|
| 522 |
+
"limit_research_materials": "Up to 1 GB per file · PDF, TXT, DOC, DOCX, CSV, XLS, XLSX",
|
| 523 |
+
"material_document_use_label": "How should SmartPaper use the uploaded PDF/DOC/DOCX files?",
|
| 524 |
+
"material_use_reference": "Use as reference literature",
|
| 525 |
+
"material_use_research": "Use as research materials",
|
| 526 |
+
"detected_method_label": "Research Design (Auto-detected)",
|
| 527 |
+
"method_detection_explanation": "SmartPaper infers the research method from the Title, Keywords, and optional materials without rewriting your Title or Keywords.",
|
| 528 |
+
"method_confirm_label": "Confirm Research Design",
|
| 529 |
+
"method_confirm_placeholder": "Select a research method",
|
| 530 |
+
"method_confirmation_required": "The current information is insufficient for reliable method detection. Please confirm the research design first.",
|
| 531 |
+
"method_not_supported": "Detected method: {method}. Its dedicated generation engine is not yet enabled, so no task will be created and no quota will be used.",
|
| 532 |
+
"method_supported": "Detected method: {method}. SmartPaper will use the corresponding research workflow.",
|
| 533 |
+
"method_undetermined": "Not yet determined",
|
| 534 |
+
"method_survey_path": "Survey and Path-Relationship Study",
|
| 535 |
+
"method_qualitative": "Qualitative Study",
|
| 536 |
+
"method_fsqca": "fsQCA Configurational Study",
|
| 537 |
+
"method_meta_analysis": "Meta-Analysis",
|
| 538 |
+
"method_panel_econometrics": "Panel Econometric Study",
|
| 539 |
+
"method_bibliometric_integrative_review": "Bibliometric and Integrative Review",
|
| 540 |
+
"adaptive_template_expander": "💡 Recommended Template (Optional)",
|
| 541 |
+
"download_research_template": "📥 Download Research Materials Template",
|
| 542 |
+
"template_help_survey_path": "A variable-level or statistical-results table is recommended. If no data are uploaded, SmartPaper may construct draft evidence under the project provenance rules.",
|
| 543 |
+
"template_help_qualitative": "Interview, field-note, or case materials are recommended. If none are uploaded, SmartPaper may construct an anonymous draft evidence packet.",
|
| 544 |
+
"template_help_fsqca": "A table of conditions, outcomes, and calibration information is recommended. The dedicated fsQCA engine will use this structure once enabled.",
|
| 545 |
+
"template_help_meta_analysis": "A study-level effect-size extraction table is recommended. Real paper identities must remain separate from any system-assisted effect-size evidence.",
|
| 546 |
+
"template_help_panel_econometrics": "An entity-time panel table, such as a firm-year structure, is recommended.",
|
| 547 |
+
"template_help_bibliometric_integrative_review": "Use real-corpus metadata such as title, authors, year, DOI, venue, and keywords.",
|
| 548 |
+
"template_help_undetermined": "Confirm the research design to view the corresponding research-material template.",
|
| 549 |
+
"start_generation_unified": "🚀 Start Generation",
|
| 550 |
"api_key_missing": "❌ DEEPSEEK_API_KEY is missing. Configure the API key.",
|
| 551 |
"resume_toast": "🔄 A page refresh or network interruption was detected. Progress is being restored automatically…",
|
| 552 |
"pipeline_start": "⏳ Starting the pipeline…",
|
|
|
|
| 688 |
},
|
| 689 |
"ru": {
|
| 690 |
"title": "📊 Интеллектуальный агент для научных статей по управлению бизнесом",
|
| 691 |
+
"topic_label": "📝 Название статьи (обязательно)",
|
| 692 |
+
"keywords_label": "🔍 Ключевые слова (обязательно)",
|
| 693 |
+
"words_label": "📏 Целевой объём текста (обязательно, {min_words}–{max_words} слов)",
|
| 694 |
+
"words_label_product": "📏 Целевой объём текста (обязательно, {min_words}–{max_words} слов)",
|
| 695 |
"product_tier_label": "Версия",
|
| 696 |
"product_tier_standard": "Стандарт",
|
| 697 |
"product_tier_flagship": "Флагман",
|
|
|
|
| 705 |
"resume_unavailable_runtime_replaced": "⚠️ Обнаружена приостановленная незавершённая задача, но в текущей среде выполнения отсутствуют локальный файл запроса или контрольная точка, необходимые для продолжения. Чтобы избежать некорректного восстановления, кнопка продолжения отключена. Для освобождения задачи и нового запуска явно выберите «Удалить и начать заново».",
|
| 706 |
"resume": "🔄 Продолжить предыдущую задачу",
|
| 707 |
"clear": "🗑️ Удалить и начать заново",
|
| 708 |
+
"cancel_generation": "⛔ Отменить",
|
| 709 |
"draft_preparing": "Подготовка черновика на английском языке…",
|
| 710 |
"draft_stage_inputs": "Систематизация исследовательских данных и доказательств…",
|
| 711 |
"draft_stage_search": "Поиск научной литературы…",
|
|
|
|
| 757 |
"asq_hint": "**⚠️ Важно**: укажите **роль информанта** и соблюдайте единый **формат диалога**, чтобы система точно извлекала цитаты и строила структуру кодирования.",
|
| 758 |
"download_asq_template": "📥 Скачать TXT-шаблон интервью",
|
| 759 |
"start_asq": "🚀 Запустить формирование в режиме ASQ",
|
| 760 |
+
"research_materials_label": "Материалы исследования (необязательно)",
|
| 761 |
+
"research_materials_help": "Поддерживаются PDF, TXT, DOC, DOCX, CSV, XLS и XLSX. SmartPaper определяет дизайн исследования и назначение материалов по названию статьи, ключевым словам и характеристикам файлов.",
|
| 762 |
+
"drop_research_materials": "Перетащите исследовательские материалы сюда",
|
| 763 |
+
"limit_research_materials": "До 1 ГБ на файл · PDF, TXT, DOC, DOCX, CSV, XLS, XLSX",
|
| 764 |
+
"material_document_use_label": "Как использовать загруженные файлы PDF/DOC/DOCX?",
|
| 765 |
+
"material_use_reference": "Как научные источники",
|
| 766 |
+
"material_use_research": "Как исследовательские материалы",
|
| 767 |
+
"detected_method_label": "Дизайн исследования (определяется автоматически)",
|
| 768 |
+
"method_detection_explanation": "SmartPaper определяет метод по названию статьи, ключевым словам и необязательным материалам, не изменяя название и ключевые слова.",
|
| 769 |
+
"method_confirm_label": "Подтвердите дизайн исследования",
|
| 770 |
+
"method_confirm_placeholder": "Выберите метод исследования",
|
| 771 |
+
"method_confirmation_required": "Имеющейся информации недостаточно для надёжного определения метода. Сначала подтвердите дизайн исследования.",
|
| 772 |
+
"method_not_supported": "Определён метод «{method}». Специализированный движок этого метода пока не включён, поэтому задача не будет создана и квота не будет списана.",
|
| 773 |
+
"method_supported": "Определён метод «{method}». SmartPaper применит соответствующий исследовательский процесс.",
|
| 774 |
+
"method_undetermined": "Пока не определён",
|
| 775 |
+
"method_survey_path": "Опросное исследование и анализ связей",
|
| 776 |
+
"method_qualitative": "Качественное исследование",
|
| 777 |
+
"method_fsqca": "Конфигурационное исследование fsQCA",
|
| 778 |
+
"method_meta_analysis": "Мета-анализ",
|
| 779 |
+
"method_panel_econometrics": "Панельное эконометрическое исследование",
|
| 780 |
+
"method_bibliometric_integrative_review": "Библиометрический и интегративный обзор",
|
| 781 |
+
"adaptive_template_expander": "💡 Рекомендуемый шаблон (необязательно)",
|
| 782 |
+
"download_research_template": "📥 Скачать шаблон исследовательских материалов",
|
| 783 |
+
"template_help_survey_path": "Рекомендуется таблица переменных или статистических результатов. При отсутствии данных SmartPaper может сформировать черновые доказательные материалы в рамках правил происхождения данных.",
|
| 784 |
+
"template_help_qualitative": "Рекомендуются интервью, полевые заметки или материалы кейсов. При их отсутствии SmartPaper может сформировать анонимный черновой пакет доказательств.",
|
| 785 |
+
"template_help_fsqca": "Рекомендуется таблица условий, результата и параметров калибровки. После включения специализированный движок fsQCA будет использовать эту структуру.",
|
| 786 |
+
"template_help_meta_analysis": "Рекомендуется таблица извлечения размеров эффектов на уровне исследований. Реальные публикации нельзя связывать с системно сформированными размерами эффектов.",
|
| 787 |
+
"template_help_panel_econometrics": "Рекомендуется панельная таблица «объект–время», например структура «компания–год».",
|
| 788 |
+
"template_help_bibliometric_integrative_review": "Используйте реальные метаданные корпуса: название, авторов, год, DOI, журнал и ключевые слова.",
|
| 789 |
+
"template_help_undetermined": "После подтверждения дизайна исследования будет показан соответствующий шаблон материалов.",
|
| 790 |
+
"start_generation_unified": "🚀 Начать",
|
| 791 |
"api_key_missing": "❌ Не задан DEEPSEEK_API_KEY. Настройте ключ API.",
|
| 792 |
"resume_toast": "🔄 Обнаружено обновление страницы или нарушение соединения. Выполняется автоматическое восстановление…",
|
| 793 |
"pipeline_start": "⏳ Запуск конвейера…",
|
|
|
|
| 929 |
},
|
| 930 |
"kk": {
|
| 931 |
"title": "📊 Бизнес-басқару жөніндегі ғылыми мақала интеллектуалды агенті",
|
| 932 |
+
"topic_label": "📝 Мақала тақырыбы (міндетті)",
|
| 933 |
+
"keywords_label": "🔍 Кілт сөздер (міндетті)",
|
| 934 |
+
"words_label": "📏 Мақсатты сөз саны (міндетті, {min_words}–{max_words} сөз)",
|
| 935 |
+
"words_label_product": "📏 Мақсатты сөз саны (міндетті, {min_words}–{max_words} сөз)",
|
| 936 |
"product_tier_label": "Нұсқа",
|
| 937 |
"product_tier_standard": "Стандарт",
|
| 938 |
"product_tier_flagship": "Флагман",
|
|
|
|
| 946 |
"resume_unavailable_runtime_replaced": "⚠️ Уақытша тоқтатылған аяқталмаған тапсырма анықталды, бірақ ағымдағы іске қосу ортасында оны жалғастыруға қажет жергілікті сұрау немесе бақылау нүктесі файлдары жоқ. Қате қалпына келтіруге жол бермеу үшін жалғастыру түймесі өшірілді. Тапс��рманы босатып, қайта бастау үшін «Өшіріп, қайта бастау» әрекетін нақты таңдаңыз.",
|
| 947 |
"resume": "🔄 Алдыңғы тапсырманы жалғастыру",
|
| 948 |
"clear": "🗑️ Өшіріп, қайта бастау",
|
| 949 |
+
"cancel_generation": "⛔ Болдырмау",
|
| 950 |
"draft_preparing": "Ағылшын тіліндегі бастапқы нұсқа дайындалуда…",
|
| 951 |
"draft_stage_inputs": "Зерттеу кірістері мен дәлелдер реттелуде…",
|
| 952 |
"draft_stage_search": "Ғылыми әдебиеттер ізделуде…",
|
|
|
|
| 998 |
"asq_hint": "**⚠️ Маңызды**: жүйе дәйексөздерді дәл шығарып, кодтау құрылымын құруы үшін **ақпарат берушінің рөлін** көрсетіп, бірізді **диалог пішімін** қолданыңыз.",
|
| 999 |
"download_asq_template": "📥 TXT сұхбат үлгісін жүктеу",
|
| 1000 |
"start_asq": "🚀 ASQ режимінде дайындауды бастау",
|
| 1001 |
+
"research_materials_label": "Зерттеу материалдары (міндетті емес)",
|
| 1002 |
+
"research_materials_help": "PDF, TXT, DOC, DOCX, CSV, XLS және XLSX форматтары қолдау табады. SmartPaper мақала атауы, түйін сөздер және файл сипаттары негізінде зерттеу дизайны мен материалдардың қызметін анықтайды.",
|
| 1003 |
+
"drop_research_materials": "Зерттеу материалдарын осы жерге сүйреп әкеліңіз",
|
| 1004 |
+
"limit_research_materials": "Әр файлға 1 ГБ дейін · PDF, TXT, DOC, DOCX, CSV, XLS, XLSX",
|
| 1005 |
+
"material_document_use_label": "Жүктелген PDF/DOC/DOCX файлдарын қалай пайдалану керек?",
|
| 1006 |
+
"material_use_reference": "Ғылыми дереккөз ретінде",
|
| 1007 |
+
"material_use_research": "Зерттеу материалы ретінде",
|
| 1008 |
+
"detected_method_label": "Зерттеу дизайны (автоматты түрде анықталады)",
|
| 1009 |
+
"method_detection_explanation": "SmartPaper зерттеу әдісін мақала атауы, түйін сөздер және қосымша материалдар негізінде анықтайды; мақала атауы мен түйін сөздерді өзгертпейді.",
|
| 1010 |
+
"method_confirm_label": "Зерттеу дизайнын растаңыз",
|
| 1011 |
+
"method_confirm_placeholder": "Зерттеу әдісін таңдаңыз",
|
| 1012 |
+
"method_confirmation_required": "Қазіргі ақпарат зерттеу әдісін сенімді анықтауға жеткіліксіз. Алдымен зерттеу дизайнын растаңыз.",
|
| 1013 |
+
"method_not_supported": "«{method}» әдісі анықталды. Бұл әдістің арнайы генерация қозғалтқышы әлі қосылмаған, сондықтан тапсырма құрылмайды және квота пайдаланылмайды.",
|
| 1014 |
+
"method_supported": "«{method}» әдісі анықталды. SmartPaper тиісті зерттеу үдерісін қолданады.",
|
| 1015 |
+
"method_undetermined": "Әзірге анықталмаған",
|
| 1016 |
+
"method_survey_path": "Сауалнама және байланыстарды талдау зерттеуі",
|
| 1017 |
+
"method_qualitative": "Сапалық зерттеу",
|
| 1018 |
+
"method_fsqca": "fsQCA конфигурациялық зерттеуі",
|
| 1019 |
+
"method_meta_analysis": "Мета-талдау",
|
| 1020 |
+
"method_panel_econometrics": "Панельдік эконометрикалық зерттеу",
|
| 1021 |
+
"method_bibliometric_integrative_review": "Библиометриялық және интегративті шолу",
|
| 1022 |
+
"adaptive_template_expander": "💡 Ұсынылатын үлгі (міндетті емес)",
|
| 1023 |
+
"download_research_template": "📥 Зерттеу материалдары үлгісін жүктеу",
|
| 1024 |
+
"template_help_survey_path": "Айнымалылар немесе статистикалық нәтижелер кестесі ұсынылады. Дерек жүктелмесе, SmartPaper жоба ережелеріне сәйкес бастапқы нұсқаға арналған көмекші дәлелдерді құра алады.",
|
| 1025 |
+
"template_help_qualitative": "Сұхбат, далалық жазба немесе кейс материалдары ұсынылады. Олар болмаса, SmartPaper анонимді бастапқы дәлелдер пакетін құра алады.",
|
| 1026 |
+
"template_help_fsqca": "Шарттар, нәтиже және калибрлеу ақпараты бар кесте ұсынылады. Арнайы fsQCA қозғалтқышы қосылғаннан кейін осы құрылым қолданылады.",
|
| 1027 |
+
"template_help_meta_analysis": "Зерттеу деңгейіндегі әсер мөлшерлерін шығару кестесі ұсынылады. Нақты мақалалардың сәйкестендіргіштері жүйе құрастырған әсер мөлшерлерімен байланыстырылмауы тиіс.",
|
| 1028 |
+
"template_help_panel_econometrics": "Нысан–уақыт панельдік кестесі, мысалы компания–жыл құрылымы ұсынылады.",
|
| 1029 |
+
"template_help_bibliometric_integrative_review": "Нақты әдеби корпус метадеректерін пайдаланыңыз: атауы, авторлары, жылы, DOI, басылымы және түйін сөздері.",
|
| 1030 |
+
"template_help_undetermined": "Зерттеу дизайнын растағаннан кейін тиісті материал үлгісі көрсетіледі.",
|
| 1031 |
+
"start_generation_unified": "🚀 Бастау",
|
| 1032 |
"api_key_missing": "❌ DEEPSEEK_API_KEY көрсетілмеген. API кілтін баптаңыз.",
|
| 1033 |
"resume_toast": "🔄 Бет жаңартылғаны немесе желі үзілісі анықталды. Үдеріс автоматты түрде қалпына келтірілуде…",
|
| 1034 |
"pipeline_start": "⏳ Жұмыс үдерісі басталуда…",
|
|
|
|
| 1204 |
return ASQ_TXT_TEMPLATES[normalize_language(language)]
|
| 1205 |
|
| 1206 |
|
| 1207 |
+
def get_research_material_template(language: str, method_family: str) -> dict[str, str]:
|
| 1208 |
+
"""Return the method-adaptive user template without changing engine policy."""
|
| 1209 |
+
family = str(method_family or "").strip().casefold()
|
| 1210 |
+
if family == "survey_path":
|
| 1211 |
+
return {
|
| 1212 |
+
"content": get_amj_csv_template(language),
|
| 1213 |
+
"code_language": "csv",
|
| 1214 |
+
"filename": "Survey_Path_Data_Template.csv",
|
| 1215 |
+
"mime_type": "text/csv",
|
| 1216 |
+
}
|
| 1217 |
+
if family == "qualitative":
|
| 1218 |
+
return {
|
| 1219 |
+
"content": get_asq_txt_template(language),
|
| 1220 |
+
"code_language": "text",
|
| 1221 |
+
"filename": "Qualitative_Research_Materials_Template.txt",
|
| 1222 |
+
"mime_type": "text/plain",
|
| 1223 |
+
}
|
| 1224 |
+
advanced = {
|
| 1225 |
+
"fsqca": (
|
| 1226 |
+
"case_id,condition_1,condition_2,condition_3,outcome\n"
|
| 1227 |
+
"CASE001,0.85,0.20,0.70,0.90\n",
|
| 1228 |
+
"fsQCA_Conditions_Outcome_Template.csv",
|
| 1229 |
+
),
|
| 1230 |
+
"meta_analysis": (
|
| 1231 |
+
"study_id,effect_size,standard_error,sample_size,moderator\n"
|
| 1232 |
+
"STUDY001,0.25,0.08,240,context_A\n",
|
| 1233 |
+
"Meta_Analysis_Effect_Size_Template.csv",
|
| 1234 |
+
),
|
| 1235 |
+
"panel_econometrics": (
|
| 1236 |
+
"entity_id,year,outcome,x1,x2,control_1\n"
|
| 1237 |
+
"E001,2025,0.62,0.40,0.71,0.15\n",
|
| 1238 |
+
"Panel_Entity_Time_Template.csv",
|
| 1239 |
+
),
|
| 1240 |
+
"bibliometric_integrative_review": (
|
| 1241 |
+
"title,authors,year,doi,venue,keywords\n"
|
| 1242 |
+
"Real publication title,Author A; Author B,2025,10.xxxx/xxxxx,Journal Name,keyword1; keyword2\n",
|
| 1243 |
+
"Bibliometric_Corpus_Metadata_Template.csv",
|
| 1244 |
+
),
|
| 1245 |
+
}
|
| 1246 |
+
content, filename = advanced.get(family, ("", "SmartPaper_Research_Materials_Template.txt"))
|
| 1247 |
+
return {
|
| 1248 |
+
"content": content,
|
| 1249 |
+
"code_language": "csv" if content else "text",
|
| 1250 |
+
"filename": filename,
|
| 1251 |
+
"mime_type": "text/csv" if content else "text/plain",
|
| 1252 |
+
}
|
| 1253 |
+
|
| 1254 |
+
|
| 1255 |
def get_reference_heading(language: str) -> str:
|
| 1256 |
return REFERENCE_HEADINGS[normalize_language(language)]
|
task_executor.py
CHANGED
|
@@ -29,6 +29,7 @@ import requests
|
|
| 29 |
|
| 30 |
from config import APP_CONFIG
|
| 31 |
from core.checkpoint_store import TaskCheckpointStore, checkpoint_authority_lock
|
|
|
|
| 32 |
|
| 33 |
_log_handlers = [logging.StreamHandler(sys.stdout)]
|
| 34 |
_executor_log_path = str(os.environ.get("SMARTPAPER_EXECUTOR_LOG_PATH") or "").strip()
|
|
@@ -415,6 +416,23 @@ def _snapshot_nullable_version(value):
|
|
| 415 |
return text or None
|
| 416 |
|
| 417 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
def _trusted_task_policy_snapshot_envelope(snapshot: dict, resolved: dict) -> dict:
|
| 419 |
return {
|
| 420 |
"task_id": str(snapshot.get("task_id") or ""),
|
|
@@ -455,11 +473,18 @@ def _validate_trusted_task_policy_snapshot(claim: dict, task: dict, request_data
|
|
| 455 |
entitlement_class = str(snapshot.get("entitlement_class") or "").strip().upper()
|
| 456 |
if tier not in {"STANDARD", "FLAGSHIP"}:
|
| 457 |
raise RuntimeError("task_policy_snapshot_product_tier_invalid")
|
| 458 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 459 |
raise RuntimeError("task_policy_snapshot_entitlement_invalid")
|
| 460 |
-
if
|
| 461 |
-
raise RuntimeError("task_policy_snapshot_entitlement_mismatch")
|
| 462 |
-
if tier == "FLAGSHIP" and entitlement_class != "FLAGSHIP_PAID":
|
| 463 |
raise RuntimeError("task_policy_snapshot_entitlement_mismatch")
|
| 464 |
|
| 465 |
if str(resolved.get("snapshot_schema_version") or "") != str(snapshot.get("snapshot_schema_version") or ""):
|
|
@@ -479,6 +504,7 @@ def _validate_trusted_task_policy_snapshot(claim: dict, task: dict, request_data
|
|
| 479 |
if _snapshot_nullable_version(resolved.get(field)) != _snapshot_nullable_version(snapshot.get(field)):
|
| 480 |
raise RuntimeError("task_policy_snapshot_policy_version_mismatch")
|
| 481 |
|
|
|
|
| 482 |
if str(resolved.get("manuscript_mode") or "").strip().lower() != str(task.get("mode") or "").strip().lower():
|
| 483 |
raise RuntimeError("task_policy_snapshot_mode_mismatch")
|
| 484 |
target_words = int(task.get("target_words") or 0)
|
|
|
|
| 29 |
|
| 30 |
from config import APP_CONFIG
|
| 31 |
from core.checkpoint_store import TaskCheckpointStore, checkpoint_authority_lock
|
| 32 |
+
from core.advanced_method_registry import METHOD_FAMILIES, method_capability
|
| 33 |
|
| 34 |
_log_handlers = [logging.StreamHandler(sys.stdout)]
|
| 35 |
_executor_log_path = str(os.environ.get("SMARTPAPER_EXECUTOR_LOG_PATH") or "").strip()
|
|
|
|
| 416 |
return text or None
|
| 417 |
|
| 418 |
|
| 419 |
+
def _validate_request_method_family(request_data: dict, task: dict) -> None:
|
| 420 |
+
"""Validate new U1-R2 method identity while preserving legacy requests."""
|
| 421 |
+
raw_family = str((request_data or {}).get("method_family") or "").strip().casefold()
|
| 422 |
+
if not raw_family:
|
| 423 |
+
return
|
| 424 |
+
if raw_family not in METHOD_FAMILIES:
|
| 425 |
+
raise RuntimeError("request_method_family_invalid")
|
| 426 |
+
capability = method_capability(raw_family)
|
| 427 |
+
expected_lane = str(capability.get("legacy_policy_lane") or "").strip().casefold()
|
| 428 |
+
request_lane = str((request_data or {}).get("derived_policy_lane") or "").strip().casefold()
|
| 429 |
+
task_lane = str((task or {}).get("mode") or "").strip().casefold()
|
| 430 |
+
if expected_lane not in {"amj", "asq"} or request_lane != expected_lane or task_lane != expected_lane:
|
| 431 |
+
raise RuntimeError("request_method_family_lane_mismatch")
|
| 432 |
+
if str(capability.get("capability_status") or "").strip().casefold() != "production_supported":
|
| 433 |
+
raise RuntimeError("request_method_family_capability_blocked")
|
| 434 |
+
|
| 435 |
+
|
| 436 |
def _trusted_task_policy_snapshot_envelope(snapshot: dict, resolved: dict) -> dict:
|
| 437 |
return {
|
| 438 |
"task_id": str(snapshot.get("task_id") or ""),
|
|
|
|
| 473 |
entitlement_class = str(snapshot.get("entitlement_class") or "").strip().upper()
|
| 474 |
if tier not in {"STANDARD", "FLAGSHIP"}:
|
| 475 |
raise RuntimeError("task_policy_snapshot_product_tier_invalid")
|
| 476 |
+
trial = resolved.get("trial") if isinstance(resolved.get("trial"), dict) else None
|
| 477 |
+
billing = resolved.get("billing") if isinstance(resolved.get("billing"), dict) else None
|
| 478 |
+
allowed_entitlements = []
|
| 479 |
+
if trial and str(trial.get("entitlement_class") or "").strip():
|
| 480 |
+
allowed_entitlements.append(str(trial.get("entitlement_class") or "").strip().upper())
|
| 481 |
+
if billing and str(billing.get("entitlement_class") or "").strip():
|
| 482 |
+
paid_class = str(billing.get("entitlement_class") or "").strip().upper()
|
| 483 |
+
if paid_class not in allowed_entitlements:
|
| 484 |
+
allowed_entitlements.append(paid_class)
|
| 485 |
+
if not entitlement_class or entitlement_class not in allowed_entitlements:
|
| 486 |
raise RuntimeError("task_policy_snapshot_entitlement_invalid")
|
| 487 |
+
if any(not value.startswith(f"{tier}_") for value in allowed_entitlements):
|
|
|
|
|
|
|
| 488 |
raise RuntimeError("task_policy_snapshot_entitlement_mismatch")
|
| 489 |
|
| 490 |
if str(resolved.get("snapshot_schema_version") or "") != str(snapshot.get("snapshot_schema_version") or ""):
|
|
|
|
| 504 |
if _snapshot_nullable_version(resolved.get(field)) != _snapshot_nullable_version(snapshot.get(field)):
|
| 505 |
raise RuntimeError("task_policy_snapshot_policy_version_mismatch")
|
| 506 |
|
| 507 |
+
_validate_request_method_family(request_data, task)
|
| 508 |
if str(resolved.get("manuscript_mode") or "").strip().lower() != str(task.get("mode") or "").strip().lower():
|
| 509 |
raise RuntimeError("task_policy_snapshot_mode_mismatch")
|
| 510 |
target_words = int(task.get("target_words") or 0)
|