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
64 changes: 62 additions & 2 deletions agents/drafter_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,59 @@ def _duration_ms(started_at: float) -> int:
return int((time.perf_counter() - started_at) * 1000)


def _build_minimal_traceability_report(state: DraftingState) -> dict[str, Any]:
claims_payload = state.get("claims") if isinstance(state.get("claims"), dict) else {}
claims = claims_payload.get("claims", []) if isinstance(claims_payload, dict) else []
reports: list[dict[str, Any]] = []
for claim in claims if isinstance(claims, list) else []:
if not isinstance(claim, dict):
continue
claim_no = int(claim.get("claim_number") or 0)
if claim_no <= 0:
continue
elements = claim.get("elements", [])
if not isinstance(elements, list) or len(elements) == 0:
fallback_text = str(claim.get("full_text", "")).strip() or "未提供可拆解的技术特征。"
elements = [fallback_text]
evidence = []
for raw_feature in elements:
feature_text = str(raw_feature).strip() or "未提供特征文本。"
evidence.append(
{
"feature_text": feature_text,
"verbatim_quote": "模型输出异常,未能稳定提取原文支持证据,请人工复核。",
"support_level": "Unsupported",
"reasoning": "自动兜底:为避免流程中断,先标记为需人工核查。",
}
)
reports.append(
{
"claim_number": claim_no,
"elements_evidence": evidence,
"is_fully_supported": False,
}
)
if not reports:
reports = [
{
"claim_number": 1,
"elements_evidence": [
{
"feature_text": "未解析到权利要求特征。",
"verbatim_quote": "模型输出异常,未能稳定提取原文支持证据,请人工复核。",
"support_level": "Unsupported",
"reasoning": "自动兜底:为避免流程中断,先标记为需人工核查。",
}
],
"is_fully_supported": False,
}
]
return {
"reports": reports,
"overall_risk_assessment": "自动兜底报告:本次溯源输出不稳定,已按最小结构补全并全部标记为需人工复核。",
}


def extract_tech_node(
state: DraftingState,
agent: BaseStructuredAgent[TechSummary],
Expand Down Expand Up @@ -171,9 +224,16 @@ def traceability_check_node(
f"[DISCLOSURE_TEXT]\n{state['disclosure_text']}\n\n"
f"[DRAFTED_CLAIMS]\n{state['claims']}"
)
result = agent.run_structured(prompt=prompt, output_model=ClaimTraceabilityReport)
try:
result = agent.run_structured(prompt=prompt, output_model=ClaimTraceabilityReport)
payload = result.model_dump()
# Node-level second pass: ensure minimum required shape even if upstream output drifts.
if not payload.get("reports"):
payload = _build_minimal_traceability_report(state)
except Exception:
payload = _build_minimal_traceability_report(state)
return {
"claim_traceability": result.model_dump(),
"claim_traceability": payload,
"current_step": "traceability_check_node",
"status": "running",
"node_latency_ms": _duration_ms(started_at),
Expand Down
31 changes: 29 additions & 2 deletions agents/oa_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,27 @@ def _duration_ms(started_at: float) -> int:
return int((time.perf_counter() - started_at) * 1000)


def _build_minimal_response_traceability_report() -> dict[str, Any]:
fallback_finding = {
"severity": "WARNING",
"risk_category": "LOGIC_INCONSISTENCY",
"problematic_text": "无",
"audit_reasoning": "模型输出异常,已触发节点级结构兜底,请人工复核最终答复文本。",
"suggested_remedy": "建议人工核查权利要求支持依据、论证一致性及不利自认风险。",
}
return {
"global_go_no_go": "NO_GO",
"support_basis_audit": [fallback_finding],
"logic_consistency_audit": [fallback_finding],
"harmful_admission_audit": [],
"final_strategy_summary": "自动兜底:响应溯源报告输出不稳定,已按最小结构补全并置为NO_GO待人工复核。",
"claim_support_ok": False,
"logic_consistency_ok": False,
"findings": [fallback_finding],
"final_risk_summary": "自动兜底:请人工复核后再提交。",
}


def _safe_int(value: Any, default: int = 0) -> int:
try:
return int(value)
Expand Down Expand Up @@ -891,8 +912,14 @@ def response_traceability_node(
f"[ARGUMENT_DRAFT (The Legal Response to Audit)]\n{state.get('argument_draft')}\n\n"
f"[STRATEGY_DECISION & STRESS TEST (For Context)]\n{state.get('strategy_decision')}\n{state.get('stress_test_report')}"
)
report = agent.run_structured(prompt=prompt, output_model=ResponseTraceabilityReport)
payload = report.model_dump()
try:
report = agent.run_structured(prompt=prompt, output_model=ResponseTraceabilityReport)
payload = report.model_dump()
# Node-level second pass to avoid whole-chain failure on semi-structured outputs.
if not isinstance(payload.get("support_basis_audit"), list):
payload = _build_minimal_response_traceability_report()
except Exception:
payload = _build_minimal_response_traceability_report()
if not payload.get("support_basis_audit") and payload.get("findings"):
payload["support_basis_audit"] = payload.get("findings", [])
if "logic_consistency_audit" not in payload:
Expand Down
104 changes: 104 additions & 0 deletions frontend/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,109 @@ fn read_input_json(input_arg: &str) -> Result<Value, String> {
}
}

fn expect_object_field(root: &Value, key: &str, errors: &mut Vec<String>) {
if let Some(v) = root.get(key) {
if !v.is_object() {
errors.push(format!("`{key}` must be an object"));
}
}
}

fn expect_string_or_null_field(root: &Value, key: &str, errors: &mut Vec<String>) {
if let Some(v) = root.get(key) {
if !(v.is_string() || v.is_null()) {
errors.push(format!("`{key}` must be a string or null"));
}
}
}

fn expect_string_array_field(root: &Value, key: &str, errors: &mut Vec<String>) {
if let Some(v) = root.get(key) {
match v.as_array() {
Some(arr) => {
for (idx, item) in arr.iter().enumerate() {
if !item.is_string() {
errors.push(format!("`{key}[{idx}]` must be a string"));
}
}
}
None => errors.push(format!("`{key}` must be an array of strings")),
}
}
}

fn validate_cli_input_schema(workflow: &str, payload: &Value) -> Result<(), String> {
let obj = payload
.as_object()
.ok_or_else(|| String::from("`--input` JSON root must be an object"))?;
let root = Value::Object(obj.clone());
let mut errors: Vec<String> = Vec::new();

if let Some(v) = root.get("idempotency_key") {
if !v.is_string() {
errors.push(String::from("`idempotency_key` must be a string"));
}
}
expect_object_field(&root, "metadata", &mut errors);

match workflow {
"draft" => {
expect_string_or_null_field(&root, "disclosure_text", &mut errors);
if let Some(v) = root.get("disclosure_file_id") {
if !(v.is_string() || v.is_null()) {
errors.push(String::from("`disclosure_file_id` must be a string or null"));
}
}
}
"oa" => {
expect_string_or_null_field(&root, "oa_text", &mut errors);
expect_object_field(&root, "original_claims", &mut errors);
expect_object_field(&root, "application_specification", &mut errors);
expect_string_array_field(&root, "prior_arts_paths", &mut errors);
expect_string_array_field(&root, "prior_art_file_ids", &mut errors);
for key in ["oa_notice_file_id", "application_file_id"] {
if let Some(v) = root.get(key) {
if !(v.is_string() || v.is_null()) {
errors.push(format!("`{key}` must be a string or null"));
}
}
}
}
"compare" => {
expect_object_field(&root, "original_claims", &mut errors);
expect_object_field(&root, "application_specification", &mut errors);
expect_string_array_field(&root, "prior_arts_paths", &mut errors);
expect_string_array_field(&root, "prior_art_file_ids", &mut errors);
for key in ["application_file_id", "comparison_goal"] {
if let Some(v) = root.get(key) {
if !(v.is_string() || v.is_null()) {
errors.push(format!("`{key}` must be a string or null"));
}
}
}
}
"polish" => {
expect_object_field(&root, "original_claims", &mut errors);
expect_object_field(&root, "application_specification", &mut errors);
if let Some(v) = root.get("application_file_id") {
if !(v.is_string() || v.is_null()) {
errors.push(String::from("`application_file_id` must be a string or null"));
}
}
}
_ => errors.push(format!("Unsupported workflow: {workflow}")),
}

if errors.is_empty() {
Ok(())
} else {
Err(format!(
"CLI input schema validation failed for workflow `{workflow}`: {}",
errors.join("; ")
))
}
}

fn detect_content_type(path: &Path) -> &'static str {
let ext = path
.extension()
Expand Down Expand Up @@ -788,6 +891,7 @@ fn execute_cli_task(opts: &CliOptions) -> Result<Value, String> {
} else {
json!({})
};
validate_cli_input_schema(workflow, &base_payload)?;
let input_json = build_cli_request_payload(opts, base_payload, base_url)?;
let response = match workflow {
"draft" => {
Expand Down
Loading