From 28a8b1562e6ef99a144957e87b926560714da0c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=BC=A0=E6=AC=A3?= <115392788+lizhangxin666@users.noreply.github.com> Date: Fri, 14 Aug 2026 00:22:42 +0800 Subject: [PATCH 1/7] feat(sim): add evidence-gated due diligence --- src/insidegov/due_diligence.py | 586 +++++++++++++++++++++ src/insidegov/due_diligence_experiments.py | 188 +++++++ src/insidegov/models.py | 131 +++++ src/insidegov/negotiation_agents.py | 86 ++- src/insidegov/negotiation_engine.py | 63 ++- src/insidegov/scenarios.py | 61 ++- src/insidegov/serde.py | 26 + tests/test_due_diligence.py | 73 +++ tests/test_negotiation.py | 40 +- 9 files changed, 1215 insertions(+), 39 deletions(-) create mode 100644 src/insidegov/due_diligence.py create mode 100644 src/insidegov/due_diligence_experiments.py create mode 100644 tests/test_due_diligence.py diff --git a/src/insidegov/due_diligence.py b/src/insidegov/due_diligence.py new file mode 100644 index 0000000..0effc7f --- /dev/null +++ b/src/insidegov/due_diligence.py @@ -0,0 +1,586 @@ +from __future__ import annotations + +import copy +import random +from dataclasses import asdict +from typing import Any + +from .agents import CognitiveProvider, DeterministicCognition, build_cognition +from .models import ( + AgentActionAudit, + DueDiligenceCase, + EvidenceItem, + ProjectRiskProfile, + WorldState, +) + +DIMENSIONS = ("financing", "technology", "market", "governance", "execution") +PROFILE_FIELDS = { + "financing": "financing_capacity", + "technology": "technology_maturity", + "market": "market_validation", + "governance": "governance_reliability", + "execution": "execution_capacity", +} +DIMENSION_NAMES = { + "financing": "资金闭合", + "technology": "技术成熟", + "market": "市场验证", + "governance": "治理信用", + "execution": "交付能力", +} +ACTION_SPECS: dict[str, dict[str, Any]] = { + "request_financing_documents": { + "dimension": "financing", "actor": "city_qing_finance", + "source": "enterprise_material", "reliability": 0.34, "cost": 0.18, "days": 3, + }, + "verify_funding_sources": { + "dimension": "financing", "actor": "city_qing_finance", + "source": "independent_verification", "reliability": 0.82, "cost": 0.72, "days": 8, + }, + "technical_expert_review": { + "dimension": "technology", "actor": "city_qing_technical", + "source": "industry_expert", "reliability": 0.84, "cost": 0.86, "days": 9, + }, + "request_technical_materials": { + "dimension": "technology", "actor": "city_qing_technical", + "source": "enterprise_material", "reliability": 0.38, "cost": 0.24, "days": 4, + }, + "customer_contract_check": { + "dimension": "market", "actor": "city_qing_investment", + "source": "counterparty_check", "reliability": 0.78, "cost": 0.65, "days": 7, + }, + "request_market_materials": { + "dimension": "market", "actor": "city_qing_investment", + "source": "enterprise_material", "reliability": 0.32, "cost": 0.18, "days": 3, + }, + "credit_and_litigation_check": { + "dimension": "governance", "actor": "city_qing_legal", + "source": "public_registry", "reliability": 0.90, "cost": 0.42, "days": 4, + }, + "request_governance_statement": { + "dimension": "governance", "actor": "city_qing_legal", + "source": "enterprise_material", "reliability": 0.36, "cost": 0.16, "days": 3, + }, + "team_delivery_reference": { + "dimension": "execution", "actor": "city_qing_technical", + "source": "third_party_reference", "reliability": 0.76, "cost": 0.58, "days": 6, + }, + "request_delivery_plan": { + "dimension": "execution", "actor": "city_qing_investment", + "source": "enterprise_material", "reliability": 0.35, "cost": 0.18, "days": 3, + }, + "red_team_challenge": { + "dimension": None, "actor": "city_qing_legal", + "source": "cross_department_red_team", "reliability": 0.88, "cost": 0.95, "days": 7, + }, +} + +PROGRAMS = { + "light_screen": { + "max_rounds": 2, + "actions": [ + "request_financing_documents", "request_technical_materials", + "request_market_materials", "request_governance_statement", "request_delivery_plan", + ], + "min_rounds": 1, + }, + "clarification_only": { + "max_rounds": 4, + "actions": [ + "request_financing_documents", "request_technical_materials", + "request_market_materials", "request_governance_statement", "request_delivery_plan", + ], + "min_rounds": 2, + }, + "independent_verification": { + "max_rounds": 5, + "actions": [ + "verify_funding_sources", "technical_expert_review", "customer_contract_check", + "credit_and_litigation_check", "team_delivery_reference", + ], + "min_rounds": 3, + }, + "red_team": { + "max_rounds": 6, + "actions": [ + "verify_funding_sources", "technical_expert_review", "customer_contract_check", + "credit_and_litigation_check", "team_delivery_reference", "red_team_challenge", + ], + "min_rounds": 4, + }, + "adaptive_staged": { + "max_rounds": 5, + "actions": list(ACTION_SPECS), + "min_rounds": 2, + }, +} + + +def protocol_due_diligence_program(protocol: str) -> str: + if protocol.upper().startswith("M"): + from .negotiation_agents import conversation_mechanism_traits + traits = conversation_mechanism_traits(protocol) + if traits.get("pre_commitment_consultation"): + return "independent_verification" + if traits.get("mutual_confirmation"): + return "adaptive_staged" + return "light_screen" + return { + "free": "light_screen", + "policy_match": "light_screen", + "clarify_first": "adaptive_staged", + "paraphrase_confirm": "clarification_only", + "constraints_first": "independent_verification", + "multi_option": "light_screen", + "phased_commitment": "adaptive_staged", + }.get(protocol, "light_screen") + + +class DueDiligenceEngine: + """Evidence-gated project screening without exposing hidden quality labels.""" + + def __init__( + self, + world: WorldState, + cognition: CognitiveProvider | None = None, + ) -> None: + self.world = world + self.cognition = cognition or build_cognition(world.policy_mode, world.model_name) + + def assess( + self, + firm_id: str, + program: str | None = None, + threshold: float | None = None, + ) -> DueDiligenceCase: + existing = next((item for item in self.world.due_diligence_cases if item.firm_id == firm_id), None) + if existing: + return existing + selected_program = program or self.world.due_diligence_program + if selected_program == "protocol_linked": + selected_program = protocol_due_diligence_program(self.world.negotiation_protocol) + if selected_program not in PROGRAMS: + raise ValueError(f"unknown due diligence program: {selected_program}") + profile = self.world.project_risk_profiles[firm_id] + firm = self.world.firms[firm_id] + stated = self.world.stated_needs[firm_id] + trust = firm.perceived_credibility.get("city_qing", 0.5) + prior = min(0.78, max(0.12, 0.24 + stated.exaggeration * 0.34 + (1 - trust) * 0.10)) + risk = { + "financing": min(0.9, prior + stated.exaggeration * 0.10), + "technology": min(0.9, prior + (0.10 if stated.category == "expansion" else 0.0)), + "market": prior, + "governance": max(0.08, prior - 0.05), + "execution": prior, + } + uncertainty_by_dimension = {dimension: 0.78 for dimension in DIMENSIONS} + config = PROGRAMS[selected_program] + evidence: list[EvidenceItem] = [] + turns: list[dict[str, Any]] = [] + available_actions = list(config["actions"]) + for round_index in range(1, int(config["max_rounds"]) + 1): + candidates = self._rank_actions( + available_actions, risk, uncertainty_by_dimension, evidence, + )[:3] + if round_index > int(config["min_rounds"]): + candidates.append("stop_and_decide") + coordinator = self.world.agents["city_qing_investment"] + observation = self._observation( + firm_id, selected_program, round_index, risk, + uncertainty_by_dimension, evidence, + ) + observation["observable_pressure_signals"] = [ + event.detail for event in self.world.events + if event.kind == "compiled_pressure_event" and event.target_id == firm_id + ] + action_id, rationale, provider, fallback, suggestion = self._choose( + coordinator, candidates, observation, + ) + if action_id == "stop_and_decide": + turns.append({ + "round": round_index, "actor_id": coordinator.id, + "action_id": action_id, "rationale": rationale, + }) + break + spec = ACTION_SPECS[action_id] + reviewer = self.world.agents[spec["actor"]] + dimension = spec["dimension"] or max( + DIMENSIONS, key=lambda item: risk[item] * uncertainty_by_dimension[item] + ) + item = self._generate_evidence( + profile, firm_id, action_id, dimension, reviewer.id, round_index, + ) + evidence.append(item) + self._update_belief(risk, uncertainty_by_dimension, item) + turns.append({ + "round": round_index, "actor_id": reviewer.id, + "action_id": action_id, "rationale": rationale, + "evidence_id": item.id, "source_type": item.source_type, + }) + self._audit( + reviewer, observation, suggestion, + { + "action_id": action_id, "dimension": dimension, + "evidence_id": item.id, "observed_quality": item.observed_quality, + "source_type": item.source_type, + }, + { + "hidden_profile_access": False, + "evidence_reliability": item.reliability, + "belief_after": round(risk[dimension], 3), + }, + rationale, provider, fallback, + ) + if action_id in available_actions: + available_actions.remove(action_id) + if not available_actions: + break + estimated = self._failure_probability(risk, evidence) + uncertainty = sum(uncertainty_by_dimension.values()) / len(DIMENSIONS) + decision_threshold = threshold if threshold is not None else self.world.due_diligence_threshold + decision, rationale, decision_turn = self._decide( + firm_id, selected_program, estimated, uncertainty, risk, + evidence, decision_threshold, + ) + turns.append(decision_turn) + actual_probability = self.actual_failure_probability(profile) + outcome_rng = random.Random(f"insidegov:due-diligence:{self.world.seed}:{firm_id}:outcome") + actual_outcome = "failed" if outcome_rng.random() < actual_probability else "succeeded" + diligence_cost = round(sum(item.cost for item in evidence), 3) + elapsed_days = sum(item.elapsed_days for item in evidence) + avoided, fiscal_loss, missed = self._settle_counterfactual_value( + profile, decision, actual_outcome, + ) + case = DueDiligenceCase( + id=f"dd-{len(self.world.due_diligence_cases)+1:04d}", + firm_id=firm_id, + program=selected_program, + prior_failure_probability=round(prior, 4), + estimated_failure_probability=round(estimated, 4), + uncertainty=round(uncertainty, 4), + decision=decision, + rationale=rationale, + rounds=len(evidence), + diligence_cost=diligence_cost, + elapsed_days=elapsed_days, + evidence=evidence, + risk_by_dimension={key: round(value, 4) for key, value in risk.items()}, + agent_turns=turns, + actual_failure_probability=round(actual_probability, 4), + actual_outcome=actual_outcome, + avoided_fiscal_loss=avoided, + realized_fiscal_loss=fiscal_loss, + missed_opportunity=missed, + ) + self.world.due_diligence_cases.append(case) + return case + + def run_all(self, program: str | None = None, threshold: float | None = None) -> list[DueDiligenceCase]: + return [self.assess(firm_id, program, threshold) for firm_id in self.world.firms] + + def _rank_actions( + self, + actions: list[str], + risk: dict[str, float], + uncertainty: dict[str, float], + evidence: list[EvidenceItem], + ) -> list[str]: + checked = {(item.action_id, item.dimension) for item in evidence} + + def value(action_id: str) -> float: + spec = ACTION_SPECS[action_id] + if action_id == "red_team_challenge": + dimension = max(DIMENSIONS, key=lambda key: risk[key] * uncertainty[key]) + else: + dimension = spec["dimension"] + novelty = 0.0 if (action_id, dimension) in checked else 1.0 + information_gain = float(spec["reliability"]) * uncertainty[dimension] + expected_loss = risk[dimension] * 0.62 + 0.18 + return novelty * information_gain * expected_loss - float(spec["cost"]) * 0.025 + + return sorted(actions, key=value, reverse=True) + + @staticmethod + def _observation( + firm_id: str, + program: str, + round_index: int, + risk: dict[str, float], + uncertainty: dict[str, float], + evidence: list[EvidenceItem], + ) -> dict[str, Any]: + return { + "firm_id": firm_id, + "program": program, + "round": round_index, + "risk_by_dimension": {key: round(value, 3) for key, value in risk.items()}, + "uncertainty_by_dimension": { + key: round(value, 3) for key, value in uncertainty.items() + }, + "evidence_seen": [ + { + "dimension": item.dimension, "source_type": item.source_type, + "observed_quality": item.observed_quality, + "reliability": item.reliability, "conflict": item.conflict, + } + for item in evidence + ], + "hidden_quality_label_available": False, + } + + def _choose(self, agent, candidates: list[str], observation: dict[str, Any]): + memories = [memory.content for memory in agent.memories[-3:]] + try: + choice = self.cognition.choose_organization_action( + agent, candidates, observation, memories, + ) + action_id = choice.action_id if choice.action_id in candidates else candidates[0] + return ( + action_id, choice.rationale, + self.cognition.model_name or self.cognition.mode, False, + choice.model_dump() if hasattr(choice, "model_dump") else asdict(choice), + ) + except Exception as exc: # noqa: BLE001 - retain autonomous run with auditable fallback + fallback = DeterministicCognition() + choice = fallback.choose_organization_action(agent, candidates, observation, memories) + return ( + choice.action_id, choice.rationale, "deterministic-fallback", True, + {"action_id": choice.action_id, "rationale": choice.rationale, + "error": f"{type(exc).__name__}: {str(exc)[:240]}"}, + ) + + def _generate_evidence( + self, + profile: ProjectRiskProfile, + firm_id: str, + action_id: str, + dimension: str, + reviewer_id: str, + round_index: int, + ) -> EvidenceItem: + spec = ACTION_SPECS[action_id] + actual_quality = float(getattr(profile, PROFILE_FIELDS[dimension])) + rng = random.Random( + f"insidegov:evidence:{self.world.seed}:{firm_id}:{action_id}:{dimension}" + ) + claim = min(1.0, max(0.0, + actual_quality + + (1 - actual_quality) * (1 - profile.candor) * 0.78 + + rng.uniform(-0.035, 0.035) + )) + reliability = float(spec["reliability"]) + if spec["source"] == "enterprise_material": + observed = claim + rng.uniform(-0.045, 0.045) + else: + observed = actual_quality + rng.uniform( + -(1 - reliability) * 0.22, (1 - reliability) * 0.22, + ) + observed = min(1.0, max(0.0, observed)) + conflict = abs(claim - observed) + return EvidenceItem( + id=f"evidence-{len(self.world.due_diligence_cases)+1:03d}-{round_index:02d}", + firm_id=firm_id, + round=round_index, + requested_by=reviewer_id, + action_id=action_id, + dimension=dimension, + source_type=str(spec["source"]), + observed_quality=round(observed, 4), + reliability=round(reliability, 3), + claim_value=round(claim, 4), + conflict=round(conflict, 4), + cost=float(spec["cost"]), + elapsed_days=int(spec["days"]), + summary=( + f"{DIMENSION_NAMES[dimension]}证据:质量 {observed:.2f}," + f"来源可靠度 {reliability:.2f},与企业主张差异 {conflict:.2f}" + ), + ) + + @staticmethod + def _update_belief( + risk: dict[str, float], uncertainty: dict[str, float], evidence: EvidenceItem, + ) -> None: + dimension = evidence.dimension + weight = evidence.reliability * (0.62 + uncertainty[dimension] * 0.28) + evidence_risk = 1 - evidence.observed_quality + risk[dimension] = min(0.98, max(0.02, + risk[dimension] * (1 - weight) + evidence_risk * weight + )) + if evidence.conflict > 0.20: + risk["governance"] = min(0.98, risk["governance"] + evidence.conflict * 0.42) + uncertainty[dimension] = max(0.06, + uncertainty[dimension] * (1 - evidence.reliability * 0.62) + ) + + @staticmethod + def _failure_probability( + risk: dict[str, float], evidence: list[EvidenceItem], + ) -> float: + weighted = ( + risk["financing"] * 0.24 + risk["technology"] * 0.22 + + risk["market"] * 0.20 + risk["governance"] * 0.14 + + risk["execution"] * 0.20 + ) + conflict = max((item.conflict * item.reliability for item in evidence), default=0.0) + return min(0.95, max(0.03, weighted + conflict * 0.22)) + + def _decide( + self, + firm_id: str, + program: str, + estimated: float, + uncertainty: float, + risk: dict[str, float], + evidence: list[EvidenceItem], + threshold: float, + ) -> tuple[str, str, dict[str, Any]]: + legal_red_flag = any( + item.dimension == "governance" and item.reliability >= 0.75 + and item.observed_quality < 0.18 + for item in evidence + ) + if legal_red_flag: + candidates = ["reject", "defer"] + elif estimated >= threshold: + candidates = ( + ["conditional_pilot", "reject", "defer"] + if program == "adaptive_staged" and uncertainty >= 0.26 + else ["reject", "conditional_pilot", "defer"] + ) + elif uncertainty >= 0.50: + if program == "adaptive_staged": + candidates = ["conditional_pilot", "defer", "approve"] + elif program in {"light_screen", "clarification_only"}: + candidates = ["approve", "defer", "reject"] + else: + candidates = ["defer", "approve", "reject"] + elif estimated >= threshold * 0.72: + candidates = ["conditional_pilot", "approve", "defer"] + else: + candidates = ["approve", "conditional_pilot", "defer"] + leader = self.world.agents["city_qing_leader"] + observation = { + "firm_id": firm_id, + "estimated_failure_probability": round(estimated, 4), + "uncertainty": round(uncertainty, 4), + "risk_by_dimension": {key: round(value, 3) for key, value in risk.items()}, + "decision_threshold": threshold, + "evidence_count": len(evidence), + "legal_red_flag": legal_red_flag, + "hidden_quality_label_available": False, + } + decision, rationale, provider, fallback, suggestion = self._choose( + leader, candidates, observation, + ) + if legal_red_flag and decision != "reject": + rule_adjustment = {"before": decision, "after": "reject", "reason": "verified_legal_red_flag"} + decision = "reject" + else: + rule_adjustment = {"before": decision, "after": decision, "reason": "authorized"} + self._audit( + leader, observation, suggestion, + {"decision": decision, "estimated_failure_probability": round(estimated, 4)}, + rule_adjustment, rationale, provider, fallback, + ) + return decision, rationale, { + "round": len(evidence) + 1, + "actor_id": leader.id, + "action_id": decision, + "rationale": rationale, + "estimated_failure_probability": round(estimated, 4), + "uncertainty": round(uncertainty, 4), + } + + def _audit( + self, agent, observation: dict, suggestion: dict, executed: dict, + adjustment: dict, rationale: str, provider: str, fallback: bool, + ) -> None: + self.world.action_audits.append(AgentActionAudit( + id=f"audit-{len(self.world.action_audits)+1:05d}", + quarter=self.world.quarter, + agent_id=agent.id, + action_type="evidence_gated_due_diligence", + observation=copy.deepcopy(observation), + private_context_used=copy.deepcopy(agent.private_facts), + retrieved_memories=[memory.content for memory in agent.memories[-3:]], + llm_suggestion=copy.deepcopy(suggestion), + rule_adjustment=copy.deepcopy(adjustment), + executed_action=copy.deepcopy(executed), + rationale=rationale, + reflection=( + "本轮只使用可见主张与核验证据;后续根据信息价值决定继续调查或停止。" + ), + provider=provider, + fallback=fallback, + outcome="executed", + )) + + @staticmethod + def actual_failure_probability(profile: ProjectRiskProfile) -> float: + latent_risk = ( + (1 - profile.financing_capacity) * 0.24 + + (1 - profile.technology_maturity) * 0.22 + + (1 - profile.market_validation) * 0.20 + + (1 - profile.governance_reliability) * 0.14 + + (1 - profile.execution_capacity) * 0.20 + ) + return min(0.92, max(0.04, (latent_risk - 0.12) * 1.32)) + + @staticmethod + def _settle_counterfactual_value( + profile: ProjectRiskProfile, decision: str, outcome: str, + ) -> tuple[float, float, float]: + full_loss = profile.requested_support + opportunity = profile.strategic_value * profile.expected_jobs / 100 + if decision == "reject": + return ( + round(full_loss, 3) if outcome == "failed" else 0.0, + 0.0, + round(opportunity, 3) if outcome == "succeeded" else 0.0, + ) + if decision == "defer": + return ( + round(full_loss * 0.85, 3) if outcome == "failed" else 0.0, + round(full_loss * 0.05, 3), + round(opportunity * 0.35, 3) if outcome == "succeeded" else 0.0, + ) + if decision == "conditional_pilot": + return ( + round(full_loss * 0.78, 3) if outcome == "failed" else 0.0, + round(full_loss * 0.22, 3) if outcome == "failed" else 0.0, + round(opportunity * 0.08, 3) if outcome == "succeeded" else 0.0, + ) + return ( + 0.0, + round(full_loss, 3) if outcome == "failed" else 0.0, + 0.0, + ) + + +def due_diligence_metrics(cases: list[DueDiligenceCase]) -> dict[str, float | int]: + flagged = {"reject", "conditional_pilot"} + tp = sum(item.actual_outcome == "failed" and item.decision in flagged for item in cases) + fn = sum(item.actual_outcome == "failed" and item.decision not in flagged for item in cases) + fp = sum(item.actual_outcome == "succeeded" and item.decision in flagged for item in cases) + tn = sum(item.actual_outcome == "succeeded" and item.decision not in flagged for item in cases) + precision = tp / (tp + fp) if tp + fp else 0.0 + recall = tp / (tp + fn) if tp + fn else 0.0 + specificity = tn / (tn + fp) if tn + fp else 0.0 + brier = sum( + (item.estimated_failure_probability - (1.0 if item.actual_outcome == "failed" else 0.0)) ** 2 + for item in cases + ) / max(len(cases), 1) + return { + "n": len(cases), "true_positive": tp, "false_negative": fn, + "false_positive": fp, "true_negative": tn, + "precision": round(precision, 4), "recall": round(recall, 4), + "specificity": round(specificity, 4), "brier_score": round(brier, 4), + "avg_rounds": round(sum(item.rounds for item in cases) / max(len(cases), 1), 3), + "avg_elapsed_days": round(sum(item.elapsed_days for item in cases) / max(len(cases), 1), 3), + "diligence_cost": round(sum(item.diligence_cost for item in cases), 3), + "avoided_fiscal_loss": round(sum(item.avoided_fiscal_loss for item in cases), 3), + "realized_fiscal_loss": round(sum(item.realized_fiscal_loss for item in cases), 3), + "missed_opportunity": round(sum(item.missed_opportunity for item in cases), 3), + } diff --git a/src/insidegov/due_diligence_experiments.py b/src/insidegov/due_diligence_experiments.py new file mode 100644 index 0000000..28b37a4 --- /dev/null +++ b/src/insidegov/due_diligence_experiments.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +import statistics +from collections import Counter +from dataclasses import asdict + +from .due_diligence import PROGRAMS, DueDiligenceEngine, due_diligence_metrics +from .scenarios import create_negotiation_world + +DEFAULT_DUE_DILIGENCE_SEEDS = [3, 11, 23, 42, 57, 89, 101, 137] +SUMMARY_METRICS = ( + "precision", "recall", "specificity", "brier_score", "avg_rounds", + "avg_elapsed_days", "diligence_cost", "avoided_fiscal_loss", + "realized_fiscal_loss", "missed_opportunity", +) + + +def run_due_diligence_matrix( + seeds: list[int] | None = None, + programs: list[str] | None = None, + thresholds: list[float] | None = None, + mode: str = "deterministic", + model_name: str | None = None, +) -> dict: + selected_seeds = seeds or DEFAULT_DUE_DILIGENCE_SEEDS + selected_programs = programs or list(PROGRAMS) + unknown = [item for item in selected_programs if item not in PROGRAMS] + if unknown: + raise ValueError(f"unknown due diligence program(s): {', '.join(unknown)}") + threshold_values = thresholds or [0.25, 0.35, 0.45, 0.50, 0.55, 0.65, 0.75] + if any(value <= 0.0 or value >= 1.0 for value in threshold_values): + raise ValueError("due diligence thresholds must be between 0 and 1") + runs: list[dict] = [] + for program in selected_programs: + for seed in selected_seeds: + world = create_negotiation_world( + seed, f"due-diligence-{program}-{seed}", "free", "plain", mode, + ) + world.model_name = model_name + world.due_diligence_program = program + cases = DueDiligenceEngine(world).run_all(program) + metrics = due_diligence_metrics(cases) + action_counts = Counter( + turn["action_id"] + for case in cases for turn in case.agent_turns + if turn.get("action_id") + ) + runs.append({ + "program": program, + "seed": seed, + **metrics, + "action_counts": dict(action_counts), + "cases": [_case_payload(world, item) for item in cases], + "agent_runtime": { + "providers": sorted({ + item.provider for item in world.action_audits if item.provider + }), + "audited_agent_decisions": len(world.action_audits), + "fallback_count": sum(bool(item.fallback) for item in world.action_audits), + }, + }) + summary = [] + for program in selected_programs: + rows = [row for row in runs if row["program"] == program] + summary.append({ + "id": program, + "name": _program_name(program), + "description": _program_description(program), + "runs": len(rows), + "metrics": { + metric: { + "mean": round(statistics.fmean(float(row[metric]) for row in rows), 5), + "variance": round(statistics.pvariance(float(row[metric]) for row in rows), 5), + } + for metric in SUMMARY_METRICS + }, + "action_counts": dict(sum( + (Counter(row["action_counts"]) for row in rows), Counter() + )), + }) + threshold_curve = _threshold_sensitivity(selected_seeds, threshold_values) + return { + "schema_version": "1.0", + "research_question": ( + "企业质量未知、披露具有策略性且尽调有成本时,何种证据获取与承诺程序" + "能最小化误签、误拒和调查成本?" + ), + "configuration": { + "seeds": selected_seeds, + "programs": selected_programs, + "thresholds": threshold_values, + "mode": mode, + "model_name": model_name, + "hidden_label_visible_to_agents": False, + }, + "agent_runtime": { + "mode": mode, + "model_name": model_name, + "providers": sorted({ + provider for run in runs + for provider in run["agent_runtime"]["providers"] + }), + "audited_agent_decisions": sum( + run["agent_runtime"]["audited_agent_decisions"] for run in runs + ), + "fallback_count": sum( + run["agent_runtime"]["fallback_count"] for run in runs + ), + "fixed_for_public_experience": mode == "llm", + }, + "program_summary": summary, + "program_runs": runs, + "threshold_curve": threshold_curve, + "decision_definitions": { + "approve": "证据支持正式协商", + "conditional_pilot": "只批准可逆试点,达标后再追加", + "defer": "补充证据后重新议程化", + "reject": "签约前否决", + }, + "metric_definitions": { + "false_positive": "实际成功,但被否决或降为风险试点", + "false_negative": "实际失败,但程序批准或仅暂缓", + "brier_score": "事前失败概率与事后结果之间的校准误差,越低越好", + }, + "interpretation_boundary": ( + "潜在因子和结果由模拟参数生成;该矩阵验证程序内部判别力," + "在真实招商应用前仍需历史项目留出验证与专家编码。" + ), + } + + +def _threshold_sensitivity(seeds: list[int], thresholds: list[float]) -> list[dict]: + rows = [] + for threshold in thresholds: + all_cases = [] + for seed in seeds: + world = create_negotiation_world( + seed, f"threshold-{threshold}-{seed}", "free", + ) + world.due_diligence_threshold = threshold + cases = DueDiligenceEngine(world).run_all( + "independent_verification", threshold, + ) + all_cases.extend(cases) + metrics = due_diligence_metrics(all_cases) + failed = metrics["true_positive"] + metrics["false_negative"] + succeeded = metrics["true_negative"] + metrics["false_positive"] + rows.append({ + "threshold": threshold, + "true_positive_rate": round(metrics["true_positive"] / failed, 4) if failed else 0.0, + "false_positive_rate": round(metrics["false_positive"] / succeeded, 4) if succeeded else 0.0, + "precision": metrics["precision"], + "specificity": metrics["specificity"], + "realized_fiscal_loss": metrics["realized_fiscal_loss"], + "missed_opportunity": metrics["missed_opportunity"], + }) + return rows + + +def _case_payload(world, case) -> dict: + firm = world.firms[case.firm_id] + return { + **asdict(case), + "firm_name": firm.name, + "initial_trust": firm.perceived_credibility.get("city_qing", 0.5), + "evidence_chain": [item.summary for item in case.evidence], + "decision_used_hidden_label": False, + } + + +def _program_name(program: str) -> str: + return { + "light_screen": "轻量筛查", + "clarification_only": "只做企业澄清", + "independent_verification": "独立证据核验", + "red_team": "跨部门反方审查", + "adaptive_staged": "自适应尽调+分阶段承诺", + }[program] + + +def _program_description(program: str) -> str: + return { + "light_screen": "低成本、快速,但主要依赖企业材料。", + "clarification_only": "增加追问轮次,仍缺少外部交叉验证。", + "independent_verification": "分别核验资金、技术、市场、治理与团队。", + "red_team": "在独立核验后增加反方挑战,优先寻找失败解释。", + "adaptive_staged": "根据当前风险与不确定性自主选择下一项证据,并保留可逆试点。", + }[program] diff --git a/src/insidegov/models.py b/src/insidegov/models.py index c3c6ba4..d0a8d05 100644 --- a/src/insidegov/models.py +++ b/src/insidegov/models.py @@ -150,6 +150,9 @@ class CityState: tax_revenue: float = 0.0 employment: int = 0 industrial_output: float = 0.0 + rescue_expenditure: float = 0.0 + imitation_expenditure: float = 0.0 + imitation_capacity: float = 0.0 landed_firms: list[str] = field(default_factory=list) active_offer: PolicyPackage | None = None @@ -186,6 +189,54 @@ class FirmState: observed_offers: dict[str, PolicyPackage] = field(default_factory=dict) tech_demand: TechDemand | None = None knowledge: float = 0.0 + lifecycle_status: str = "active" + consecutive_losses: int = 0 + distress_quarters: int = 0 + rescue_count: int = 0 + rescue_received: float = 0.0 + exit_quarter: int | None = None + + +@dataclass(slots=True) +class ImitationDecision: + """A rival city's observable response to another city's industrial success.""" + + id: str + quarter: int + city_id: str + source_city_id: str + observed_signal: dict[str, float] + strategy: str + requested_cost: float + approved_cost: float + added_capacity: float + added_jobs: int + created_firm_id: str | None + rationale: str + provider: str = "deterministic" + turns: list[dict[str, Any]] = field(default_factory=list) + + +@dataclass(slots=True) +class RescueDecision: + """Government-enterprise rescue meeting followed by rule-engine settlement.""" + + id: str + quarter: int + city_id: str + firm_id: str + requested_amount: float + finance_limit: float + decision: str + approved_amount: float + conditional: bool + capacity_before: float + capacity_after: float + jobs_before: int + jobs_after: int + rationale: str + provider: str = "deterministic" + turns: list[dict[str, Any]] = field(default_factory=list) @dataclass(slots=True) @@ -540,6 +591,12 @@ class MetricsSnapshot: regret_rate: float = 0.0 total_gov_cost: float = 0.0 total_ent_commitment: float = 0.0 + distressed_firms: int = 0 + rescued_firms: int = 0 + exited_firms: int = 0 + zombie_firms: int = 0 + rescue_spending: float = 0.0 + imitation_capacity: float = 0.0 @dataclass(slots=True) @@ -561,6 +618,8 @@ class WorldState: organization_actions: list[OrganizationActionRecord] history: list[MetricsSnapshot] interventions: list[Intervention] + imitation_decisions: list[ImitationDecision] = field(default_factory=list) + rescue_decisions: list[RescueDecision] = field(default_factory=list) organization_plans: list[OrganizationPlan] = field(default_factory=list) open_action_proposals: list[OpenActionProposal] = field(default_factory=list) opportunity_windows: list[OpportunityWindow] = field(default_factory=list) @@ -576,6 +635,8 @@ class WorldState: selected_city_id: str | None = None recruitment_status: str = "active" negotiation_round_limit: int = 6 + imitation_policy: str = "adaptive" + rescue_policy: str = "adaptive" parent_id: str | None = None policy_mode: str = "deterministic" model_name: str | None = None @@ -586,6 +647,9 @@ class WorldState: "internal_governance": True, "credibility_diffusion": True, "supplier_spillover": True, + "city_imitation": True, + "enterprise_exit": True, + "government_rescue": True, }) talents: dict[str, TalentState] = field(default_factory=dict) universities: dict[str, UniversityState] = field(default_factory=dict) @@ -603,6 +667,14 @@ class WorldState: ent_beliefs: dict[str, EntBelief] = field(default_factory=dict) negotiation_records: list[NegotiationRecord] = field(default_factory=list) cooperation_executions: list[CooperationExecution] = field(default_factory=list) + project_risk_profiles: dict[str, ProjectRiskProfile] = field(default_factory=dict) + due_diligence_cases: list[DueDiligenceCase] = field(default_factory=list) + due_diligence_program: str = "protocol_linked" + due_diligence_threshold: float = 0.5 + # Public-experience actions are requests, not direct state mutations. The + # organization engine validates each directive against the actor's role, + # current process arena and hard rules before it can affect the world. + experience_directives: list[dict[str, Any]] = field(default_factory=list) def to_dict(self) -> dict[str, Any]: return asdict(self) @@ -728,12 +800,71 @@ class LatentNeed: commitment: float required_tools: dict[str, float] truth: dict[str, float] + # Deprecated compatibility field. Screening decisions must never read this + # oracle label; ProjectRiskProfile generates evidence and ex-post outcomes. unfeasible: bool = False def model_dump(self) -> dict[str, Any]: return asdict(self) +@dataclass(slots=True) +class ProjectRiskProfile: + """Hidden outcome-generating factors; organization agents cannot read these.""" + + firm_id: str + financing_capacity: float + technology_maturity: float + market_validation: float + governance_reliability: float + execution_capacity: float + candor: float + strategic_value: float + expected_jobs: int + requested_support: float + + +@dataclass(slots=True) +class EvidenceItem: + id: str + firm_id: str + round: int + requested_by: str + action_id: str + dimension: str + source_type: str + observed_quality: float + reliability: float + claim_value: float + conflict: float + cost: float + elapsed_days: int + summary: str + + +@dataclass(slots=True) +class DueDiligenceCase: + id: str + firm_id: str + program: str + prior_failure_probability: float + estimated_failure_probability: float + uncertainty: float + decision: str + rationale: str + rounds: int + diligence_cost: float + elapsed_days: int + evidence: list[EvidenceItem] = field(default_factory=list) + risk_by_dimension: dict[str, float] = field(default_factory=dict) + agent_turns: list[dict[str, Any]] = field(default_factory=list) + actual_failure_probability: float = 0.0 + actual_outcome: str = "unrevealed" + avoided_fiscal_loss: float = 0.0 + realized_fiscal_loss: float = 0.0 + missed_opportunity: float = 0.0 + + @dataclass(slots=True) class StatedNeed: firm_id: str diff --git a/src/insidegov/negotiation_agents.py b/src/insidegov/negotiation_agents.py index d57718e..d39c470 100644 --- a/src/insidegov/negotiation_agents.py +++ b/src/insidegov/negotiation_agents.py @@ -24,7 +24,7 @@ from typing import Any import httpx -from pydantic import BaseModel, Field, ValidationError +from pydantic import BaseModel, Field, ValidationError, field_validator from .models import AgentState, CityState, FirmState, GovBelief, LatentNeed, StatedNeed, WorldState @@ -48,6 +48,29 @@ # 澄清追问的顺序:问题本质与合作方式最优先 CLARIFY_PRIORITY = ["problem", "mode", "target", "deadline", "constraint", "budget", "commitment"] +# G-E style compositional mechanisms. The identifier encodes three binary +# dimensions while the existing InsideGov negotiation engine remains the sole +# state and settlement authority. +CONVERSATION_MECHANISMS: dict[str, dict[str, Any]] = {} +for _index, (_confirm, _preconsult, _conditional) in enumerate( + ( + (False, False, False), (False, False, True), + (False, True, False), (False, True, True), + (True, False, False), (True, False, True), + (True, True, False), (True, True, True), + ), + 1, +): + CONVERSATION_MECHANISMS[f"M{_index}"] = { + "mutual_confirmation": _confirm, + "pre_commitment_consultation": _preconsult, + "conditional_commitment": _conditional, + } + + +def conversation_mechanism_traits(protocol: str) -> dict[str, bool]: + return dict(CONVERSATION_MECHANISMS.get(protocol.upper(), {})) + class DiagnoseAction(BaseModel): next_action: str # clarify / paraphrase / disclose / propose @@ -73,6 +96,21 @@ class ProposalAction(BaseModel): options: list[dict[str, Any]] = Field(default_factory=list) rationale: str + @field_validator("options") + @classmethod + def validate_executable_options(cls, options: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Reject prose-shaped LLM proposals before the rules try to execute them.""" + for option in options: + tools = option.get("tools") + if not isinstance(tools, dict): + raise TypeError("proposal tools must be a name-to-strength object") + if any(not isinstance(value, (int, float)) for value in tools.values()): + raise TypeError("proposal tool strength must be numeric") + conditions = option.get("conditions", []) + if not isinstance(conditions, list) or any(not isinstance(item, str) for item in conditions): + raise TypeError("proposal conditions must be a list of strings") + return options + class EvaluateAction(BaseModel): response: str # accept / counter / terminate @@ -164,6 +202,26 @@ def diagnose( round_index: int, world: WorldState, ) -> DiagnoseAction: + traits = conversation_mechanism_traits(protocol) + if traits: + if traits["pre_commitment_consultation"] and round_index == 0: + return DiagnoseAction( + next_action="disclose", + rationale="承诺前会商:政府先公开权限、程序与预算边界", + ) + diagnostic_round = round_index - int(traits["pre_commitment_consultation"]) + if traits["mutual_confirmation"] and not self._understanding_ok(belief): + start = max(0, diagnostic_round) * 3 + questions = CLARIFY_PRIORITY[start:start + 3] + if questions: + return DiagnoseAction( + next_action="clarify", questions=questions, + rationale="双方确认需求:在形成承诺前分批追问关键成分", + ) + return DiagnoseAction( + next_action="propose", + rationale="协议规定的信息步骤已完成,进入政策方案形成", + ) if protocol == "policy_match": return DiagnoseAction( next_action="propose", rationale="政策匹配:按关键词立即推荐,不追问" @@ -257,6 +315,7 @@ def propose( protocol: str, world: WorldState, ) -> ProposalAction: + traits = conversation_mechanism_traits(protocol) if belief.components.get("mode", 0.0) >= 0.50 and belief.perceived_mode: toolset = MODE_TOOLS.get(belief.perceived_mode, CATEGORY_TOOLS.get(stated.category, ["tech_contract"])) else: @@ -288,7 +347,7 @@ def propose( "conditions": ["企业自有投入不低于 60%", "分两期验收后拨付"], "phased": True, }, ] - elif protocol == "phased_commitment": + elif protocol == "phased_commitment" or traits.get("conditional_commitment", False): options = [{ "label": "分阶段方案", "tools": {t: round(s * 0.55, 2) for t, s in base_tools.items()}, @@ -343,8 +402,10 @@ def evaluate( understanding = self._language_understanding(option.get("language_style", world.language_style)) trust_after = trust + (0.06 if option.get("discloses_limits") else 0.0) + (0.05 if option.get("phased") else 0.0) score = perceived_fit * 0.45 + understanding * 0.20 + trust_after * 0.20 + feasibility * 0.15 - # 亟需资金的夸大/高风险企业更看重支持力度而非匹配(若政府未识别,容易误签) - needy = latent.unfeasible or stated.exaggeration > 0.25 + # 企业侧只依据自己的资金投入能力和表达策略作出响应。这里不再读取 + # 研究者预置的质量标签,因而“看起来可信但自筹不足”的企业仍可能 + # 主动追逐高额支持,政府只能通过后续证据程序识别这种风险。 + needy = latent.commitment < 0.30 or stated.exaggeration > 0.25 if needy: support_norm = min(1.0, sum( TOOL_COST.get(tool, 0.0) * value for tool, value in option.get("tools", {}).items() @@ -391,6 +452,11 @@ def _mean(values) -> float: @staticmethod def _style_for(protocol: str, world_style: str) -> str: + traits = conversation_mechanism_traits(protocol) + if traits: + if traits["pre_commitment_consultation"]: + return "frank" + return "plain" if traits["conditional_commitment"] else "formal" if protocol == "constraints_first": return "frank" if protocol in ("clarify_first", "phased_commitment"): @@ -480,13 +546,19 @@ def _context(self, agent: AgentState, extra: dict[str, Any]) -> dict[str, Any]: } def diagnose(self, agent, city, firm, stated, belief, protocol, round_index, world) -> DiagnoseAction: + traits = conversation_mechanism_traits(protocol) return self._ask( # type: ignore[return-value] "政府:决定下一步协商动作(追问/复述/披露约束/提案)", DiagnoseAction, self._context(agent, { "protocol": protocol, "round": round_index, "expression": stated.text, "current_understanding": belief.components, "perceived_mode": belief.perceived_mode, - "allowed_next": ["clarify", "paraphrase", "disclose", "propose"], + "mechanism_traits": traits, + "allowed_next": ( + ["disclose"] if traits.get("pre_commitment_consultation") and round_index == 0 + else ["clarify", "propose"] if traits.get("mutual_confirmation") + else ["propose"] if traits else ["clarify", "paraphrase", "disclose", "propose"] + ), "output_schema": DiagnoseAction.model_json_schema(), }), ) @@ -517,7 +589,9 @@ def propose(self, agent, city, firm, stated, belief, protocol, world) -> Proposa "政府:基于当前理解生成政策方案包", ProposalAction, self._context(agent, { - "protocol": protocol, "expression": stated.text, + "protocol": protocol, + "mechanism_traits": conversation_mechanism_traits(protocol), + "expression": stated.text, "understanding": belief.components, "perceived_mode": belief.perceived_mode, "budget_headroom": city.available_budget, "allowed_tools": list(TOOL_COST), diff --git a/src/insidegov/negotiation_engine.py b/src/insidegov/negotiation_engine.py index 96c0f8b..2705796 100644 --- a/src/insidegov/negotiation_engine.py +++ b/src/insidegov/negotiation_engine.py @@ -24,6 +24,7 @@ import httpx +from .due_diligence import DueDiligenceEngine from .models import ( CooperationExecution, DecisionTrace, @@ -41,6 +42,7 @@ DeterministicNegotiationCognition, NegotiationCognitiveProvider, build_negotiation_cognition, + conversation_mechanism_traits, ) COMPONENTS = ["problem", "target", "deadline", "budget", "mode", "constraint", "commitment"] @@ -60,6 +62,7 @@ def __init__( self.world.model_name = self.cognition.model_name self.world.phase = Phase.NEGOTIATION self.random = random.Random(world.seed) + self.due_diligence = DueDiligenceEngine(world) def step(self) -> WorldState: self.world.quarter += 1 @@ -97,12 +100,27 @@ def _negotiate(self, firm_id: str) -> NegotiationRecord | None: latent = self.world.latent_needs[firm_id] stated = self.world.stated_needs[firm_id] protocol = self.world.negotiation_protocol + mechanism_traits = conversation_mechanism_traits(protocol) city = self.world.cities["city_qing"] gov_agent = self.world.agents["city_qing_investment"] firm_agent = self.world.agents[f"{firm_id}_board"] firm = self.world.firms[firm_id] belief = self.world.gov_beliefs[firm_id] self._init_belief(belief, stated, latent) + diligence = self.due_diligence.assess(firm_id) + if diligence.decision == "reject": + return self._terminate( + firm_id, + f"证据门控尽调否决:预计失败概率 {diligence.estimated_failure_probability:.0%};" + f"{diligence.rationale}", + belief, self._gap(belief), [], 0, 0, 0.0, gov_agent, + ) + if diligence.decision == "defer": + return self._terminate( + firm_id, + f"证据不足,暂缓签约:不确定性 {diligence.uncertainty:.0%};{diligence.rationale}", + belief, self._gap(belief), [], 0, 0, 0.0, gov_agent, + ) turns: list[dict] = [] clarifications = 0 @@ -113,6 +131,19 @@ def _negotiate(self, firm_id: str) -> NegotiationRecord | None: round_index = 0 while round_index < MAX_NEGOTIATION_ROUNDS: diag = self._cognitive_call("diagnose", gov_agent, city, firm, stated, belief, protocol, round_index, self.world) + # A compositional protocol constrains the legal sequence, not the + # Agent's substantive judgment. If an LLM skips a mandatory + # consultation/confirmation step, the protocol gate repairs only + # the next-action type and leaves later proposal content autonomous. + if mechanism_traits: + expected = DeterministicNegotiationCognition().diagnose( + gov_agent, city, firm, stated, belief, protocol, round_index, self.world, + ) + if diag.next_action not in { + expected.next_action, + "propose" if expected.next_action == "clarify" and self._gap(belief) <= 0.12 else expected.next_action, + }: + diag = expected round_index += 1 if diag.next_action == "propose": break @@ -150,24 +181,29 @@ def _negotiate(self, firm_id: str) -> NegotiationRecord | None: if not options: return self._terminate(firm_id, "政府未生成任何可行方案", belief, gap_initial, turns, clarifications, paraphrases, 0.0, gov_agent) + if mechanism_traits.get("conditional_commitment"): + for option in options: + option["phased"] = True + conditions = list(option.get("conditions", [])) + if "首期试点验收后再追加支持" not in conditions: + conditions.append("首期试点验收后再追加支持") + option["conditions"] = conditions + option["discloses_limits"] = True if protocol == "multi_option" and len(options) > 1: option = self._choose_option(firm_agent, latent, stated, options) self._apply_preference(belief, latent) else: option = options[0] + if diligence.decision == "conditional_pilot": + option["phased"] = True + option["tools"] = { + key: round(value * 0.35, 3) + for key, value in option.get("tools", {}).items() + } + option["due_diligence_condition"] = "pilot_before_full_commitment" fit = self._true_fit(latent, option) trust = firm.perceived_credibility.get("city_qing", 0.55) - # ---- 政府侧低质项目识别:只要看清企业真实投入(commitment), - # 就足以识别「技术未成熟 + 无自筹能力」的项目并在签约前叫停 ---- - if latent.unfeasible and belief.components.get("commitment", 0.0) >= 0.02: - self._event( - "veto", "识别低质项目", f"{firm.name}技术未成熟且无自筹能力,签约前叫停", - "city_qing_planning", firm_id, "warning", - ) - return self._terminate(firm_id, "识别为低质项目:技术未成熟且企业无自筹能力", - belief, gap_initial, turns, clarifications, paraphrases, fit, gov_agent) - # ---- 阶段三:企业评估与还价 ---- evaluation = self._cognitive_call("evaluate", firm_agent, latent, stated, option, trust, self.world) turns.append({ @@ -217,10 +253,15 @@ def _sign( gap_final = self._gap(belief) semantic = round(1.0 - gap_final, 3) style = option.get("language_style", self.world.language_style) + risk_profile = self.world.project_risk_profiles.get(firm_id) + latent_failure_risk = ( + self.due_diligence.actual_failure_probability(risk_profile) + if risk_profile is not None else 0.35 + ) success_prob = round(max(0.05, min(0.95, 0.35 + fit * 0.30 + ent_commit * 0.25 + (0.15 if option.get("phased") else 0.0) - - 0.30 * stated.exaggeration - (0.30 if latent.unfeasible else 0.0) + - 0.30 * stated.exaggeration - 0.35 * latent_failure_risk )), 4) record = NegotiationRecord( diff --git a/src/insidegov/scenarios.py b/src/insidegov/scenarios.py index 0da1324..1e0b549 100644 --- a/src/insidegov/scenarios.py +++ b/src/insidegov/scenarios.py @@ -15,6 +15,7 @@ OrganizationProcessState, Phase, PlatformState, + ProjectRiskProfile, StatedNeed, TalentState, TalentType, @@ -497,16 +498,29 @@ def create_negotiation_world( world.talents, world.universities, world.platform = {}, {}, None world.latent_needs, world.stated_needs = {}, {} world.gov_beliefs, world.ent_beliefs = {}, {} + world.agents["city_qing_legal"] = AgentState( + id="city_qing_legal", name="青禾市法务与信用审查组", role=AgentRole.LEGAL, + owner_id="city_qing", goals=["核验主体与实控人", "识别材料矛盾", "守住合规红线"], + private_facts={"hard_red_line": 0.78, "verification_budget": 4.0}, + traits={"risk_aversion": 0.84, "short_termism": 0.18, "trust_sensitivity": 0.36}, + ) + world.agents["city_qing_technical"] = AgentState( + id="city_qing_technical", name="青禾市行业技术评审组", role=AgentRole.PARK, + owner_id="city_qing", goals=["验证技术成熟度", "核验市场和量产条件", "避免热度替代证据"], + private_facts={"pilot_preference": 0.68, "verification_budget": 5.0}, + traits={"risk_aversion": 0.62, "short_termism": 0.22, "trust_sensitivity": 0.30}, + ) profiles = [ - ("firm_bio", "蓝芯生物", "talent_shortage", "flexible", "建立高校柔性研发合作", 0.38, 0.12, False), - ("firm_chip", "微核传感", "talent_shortage", "pilot", "完成传感芯片中试验证", 0.42, 0.10, False), - ("firm_data", "澄数科技", "digitalization", "project", "完成工业数据治理项目", 0.62, 0.08, False), - ("firm_clean", "清源材料", "consulting", "diagnosis", "诊断材料量产良率问题", 0.66, 0.06, False), - ("firm_control", "睿控装备", "expansion", "capacity", "扩建智能控制产线", 0.68, 0.08, False), - ("firm_med", "禾康器械", "consulting", "diagnosis", "完成医疗器械工程验证", 0.59, 0.10, False), - ("firm_robot", "灵虎机器人", "digitalization", "digital", "升级产线数字控制系统", 0.56, 0.12, False), - ("firm_risky", "高能电池", "expansion", "pilot", "验证尚未成熟的高能电池技术", 0.44, 0.55, True), + # 最后一列是企业自己知道的资金投入能力,不是政府可见的质量标签。 + ("firm_bio", "蓝芯生物", "talent_shortage", "flexible", "建立高校柔性研发合作", 0.38, 0.12, 0.62), + ("firm_chip", "微核传感", "talent_shortage", "pilot", "完成传感芯片中试验证", 0.42, 0.10, 0.62), + ("firm_data", "澄数科技", "digitalization", "project", "完成工业数据治理项目", 0.62, 0.08, 0.62), + ("firm_clean", "清源材料", "consulting", "diagnosis", "诊断材料量产良率问题", 0.66, 0.06, 0.62), + ("firm_control", "睿控装备", "expansion", "capacity", "扩建智能控制产线", 0.68, 0.08, 0.62), + ("firm_med", "禾康器械", "consulting", "diagnosis", "完成医疗器械工程验证", 0.59, 0.10, 0.62), + ("firm_robot", "灵虎机器人", "digitalization", "digital", "升级产线数字控制系统", 0.40, 0.24, 0.58), + ("firm_risky", "高能电池", "expansion", "pilot", "验证尚未成熟的高能电池技术", 0.78, 0.55, 0.15), ] firms: dict[str, FirmState] = {} toolsets = { @@ -518,7 +532,7 @@ def create_negotiation_world( "capacity": {"industry_fund": 0.8, "equipment_subsidy": 0.8, "tax_credit": 0.6}, } for index, profile in enumerate(profiles): - firm_id, name, category, preferred_mode, problem, trust, exaggeration, unfeasible = profile + firm_id, name, category, preferred_mode, problem, trust, exaggeration, commitment = profile firm = world.firms.get(firm_id) or FirmState( firm_id, name, FirmType.TECHNOLOGY, 28.0 + index * 2, 9.0 + index, 10.0, 70 + index * 8, 4.0, 25.0 + index * 3, @@ -530,16 +544,19 @@ def create_negotiation_world( world.agents[f"{firm_id}_board"] = AgentState( id=f"{firm_id}_board", name=f"{name}决策团队", role=AgentRole.ENTERPRISE, owner_id=firm_id, goals=["解决真实经营问题", "控制合作成本", "保护商业边界"], - private_facts={"true_mode": preferred_mode, "unfeasible": unfeasible}, + private_facts={ + "true_mode": preferred_mode, + "own_commitment": commitment, + }, traits={"risk_aversion": 0.58, "short_termism": 0.48, "trust_sensitivity": 0.72}, ) world.latent_needs[firm_id] = LatentNeed( firm_id, problem, preferred_mode, 12, 42.0 + index * 2, ["不接受全职引才", "核心数据与客户名单不得披露"] if preferred_mode in {"flexible", "pilot"} else ["分期验收"], - 0.12 if unfeasible else 0.62, toolsets[preferred_mode], + commitment, toolsets[preferred_mode], {"problem": 0.96, "target": 0.90, "deadline": 0.78, "budget": 0.72, - "mode": 0.94, "constraint": 0.86, "commitment": 0.15 if unfeasible else 0.76}, - unfeasible=unfeasible, + "mode": 0.94, "constraint": 0.86, "commitment": commitment}, + unfeasible=False, ) disclosed = 0.14 if trust < 0.50 else 0.48 world.stated_needs[firm_id] = StatedNeed( @@ -553,4 +570,22 @@ def create_negotiation_world( world.gov_beliefs[firm_id] = GovBelief(firm_id) world.ent_beliefs[firm_id] = EntBelief(firm_id, trust=trust) world.firms = firms + # These profiles generate evidence and ex-post outcomes. Government agents + # never receive them directly; they only observe claims and requested checks. + risk_values = { + "firm_bio": (0.74, 0.78, 0.66, 0.82, 0.72, 0.80, 0.76, 150, 7.0), + "firm_chip": (0.68, 0.74, 0.64, 0.76, 0.70, 0.74, 0.82, 180, 8.0), + "firm_data": (0.82, 0.84, 0.80, 0.86, 0.78, 0.90, 0.70, 120, 5.0), + "firm_clean": (0.76, 0.72, 0.74, 0.80, 0.82, 0.88, 0.72, 210, 8.0), + "firm_control": (0.80, 0.86, 0.78, 0.84, 0.86, 0.84, 0.86, 420, 12.0), + "firm_med": (0.70, 0.76, 0.71, 0.78, 0.74, 0.86, 0.68, 160, 6.0), + # Low-trust and genuinely fragile: useful for separating distrust from risk. + "firm_robot": (0.46, 0.62, 0.38, 0.58, 0.50, 0.72, 0.60, 240, 9.0), + # Persuasive/high-trust but weak fundamentals and low candor. + "firm_risky": (0.28, 0.24, 0.34, 0.42, 0.38, 0.24, 0.88, 680, 18.0), + } + world.project_risk_profiles = { + firm_id: ProjectRiskProfile(firm_id, *values) + for firm_id, values in risk_values.items() + } return world diff --git a/src/insidegov/serde.py b/src/insidegov/serde.py index 236d58f..7628f89 100644 --- a/src/insidegov/serde.py +++ b/src/insidegov/serde.py @@ -8,12 +8,15 @@ CooperationExecution, DecisionTrace, DepartmentState, + DueDiligenceCase, EntBelief, Event, + EvidenceItem, ExternalNegotiationRound, FirmState, FirmType, GovBelief, + ImitationDecision, Intervention, InterventionChange, InterventionPlan, @@ -35,8 +38,10 @@ PlanStrategyOption, PlatformState, PolicyPackage, + ProjectRiskProfile, Promise, PromiseStatus, + RescueDecision, StatedNeed, TalentContract, TalentNegotiation, @@ -147,6 +152,12 @@ def world_from_dict(data: dict) -> WorldState: }, history=[MetricsSnapshot(**{**h, "phase": Phase(h["phase"])}) for h in data.get("history", [])], interventions=[Intervention(**i) for i in data.get("interventions", [])], + imitation_decisions=[ + ImitationDecision(**item) for item in data.get("imitation_decisions", []) + ], + rescue_decisions=[ + RescueDecision(**item) for item in data.get("rescue_decisions", []) + ], intervention_plans=[InterventionPlan(**{ **item, "changes": [InterventionChange(**change) for change in item.get("changes", [])], @@ -162,6 +173,8 @@ def world_from_dict(data: dict) -> WorldState: market_price=data.get("market_price", 1.0), selected_city_id=data.get("selected_city_id"), recruitment_status=data.get("recruitment_status", "active"), negotiation_round_limit=data.get("negotiation_round_limit", 6), + imitation_policy=data.get("imitation_policy", "adaptive"), + rescue_policy=data.get("rescue_policy", "adaptive"), parent_id=data.get("parent_id"), policy_mode=data.get("policy_mode", "deterministic"), model_name=data.get("model_name"), process_mode=data.get("process_mode", "hybrid"), @@ -175,6 +188,8 @@ def world_from_dict(data: dict) -> WorldState: mechanisms=data.get("mechanisms", { "private_information": True, "internal_governance": True, "credibility_diffusion": True, "supplier_spillover": True, + "city_imitation": True, "enterprise_exit": True, + "government_rescue": True, }), talents={ key: TalentState(**{**item, "talent_type": TalentType(item["talent_type"])}) @@ -194,4 +209,15 @@ def world_from_dict(data: dict) -> WorldState: ent_beliefs={key: EntBelief(**item) for key, item in data.get("ent_beliefs", {}).items()}, negotiation_records=[NegotiationRecord(**item) for item in data.get("negotiation_records", [])], cooperation_executions=[CooperationExecution(**item) for item in data.get("cooperation_executions", [])], + project_risk_profiles={ + key: ProjectRiskProfile(**item) + for key, item in data.get("project_risk_profiles", {}).items() + }, + due_diligence_cases=[DueDiligenceCase(**{ + **item, + "evidence": [EvidenceItem(**row) for row in item.get("evidence", [])], + }) for item in data.get("due_diligence_cases", [])], + due_diligence_program=data.get("due_diligence_program", "protocol_linked"), + due_diligence_threshold=data.get("due_diligence_threshold", 0.5), + experience_directives=data.get("experience_directives", []), ) diff --git a/tests/test_due_diligence.py b/tests/test_due_diligence.py new file mode 100644 index 0000000..9ea4beb --- /dev/null +++ b/tests/test_due_diligence.py @@ -0,0 +1,73 @@ +from insidegov.due_diligence import DueDiligenceEngine, due_diligence_metrics +from insidegov.due_diligence_experiments import run_due_diligence_matrix +from insidegov.scenarios import create_negotiation_world +from insidegov.serde import world_from_dict +from insidegov.api import _public_world + + +def _cases(seed: int, program: str, threshold: float = 0.5): + world = create_negotiation_world(seed, f"test-{program}-{seed}", "free") + world.due_diligence_program = program + return world, DueDiligenceEngine(world).run_all(program, threshold) + + +def test_agents_never_receive_hidden_risk_profile() -> None: + world, cases = _cases(3, "adaptive_staged") + assert cases + for audit in world.action_audits: + if audit.action_type != "evidence_gated_due_diligence": + continue + assert audit.observation["hidden_quality_label_available"] is False + assert "financing_capacity" not in audit.observation + assert audit.rule_adjustment.get("hidden_profile_access") is False or ( + audit.rule_adjustment.get("reason") in {"authorized", "verified_legal_red_flag"} + ) + + +def test_independent_evidence_separates_claim_from_observation() -> None: + _, cases = _cases(3, "independent_verification") + risky = next(item for item in cases if item.firm_id == "firm_risky") + assert risky.evidence + assert any(item.source_type != "enterprise_material" for item in risky.evidence) + assert any(item.conflict > 0.15 for item in risky.evidence) + assert risky.estimated_failure_probability > risky.prior_failure_probability + + +def test_programs_trade_accuracy_for_cost_and_reversibility() -> None: + report = run_due_diligence_matrix(seeds=[3, 11, 23, 42], thresholds=[0.35, 0.5, 0.75]) + summary = {item["id"]: item for item in report["program_summary"]} + light = summary["light_screen"]["metrics"] + verified = summary["independent_verification"]["metrics"] + adaptive = summary["adaptive_staged"]["metrics"] + assert verified["recall"]["mean"] > light["recall"]["mean"] + assert verified["diligence_cost"]["mean"] > light["diligence_cost"]["mean"] + assert adaptive["missed_opportunity"]["mean"] < verified["missed_opportunity"]["mean"] + assert len(report["threshold_curve"]) == 3 + assert report["threshold_curve"][0]["false_positive_rate"] >= report["threshold_curve"][-1]["false_positive_rate"] + + +def test_due_diligence_metrics_count_both_error_types() -> None: + _, cases = _cases(3, "independent_verification") + metrics = due_diligence_metrics(cases) + assert metrics["true_positive"] > 0 + assert metrics["false_positive"] >= 0 + assert metrics["true_positive"] + metrics["false_negative"] == sum( + item.actual_outcome == "failed" for item in cases + ) + + +def test_due_diligence_round_trip() -> None: + world, _ = _cases(3, "adaptive_staged") + restored = world_from_dict(world.to_dict()) + assert restored.project_risk_profiles.keys() == world.project_risk_profiles.keys() + assert len(restored.due_diligence_cases) == len(world.due_diligence_cases) + assert restored.due_diligence_cases[0].evidence[0].source_type + + +def test_public_world_redacts_outcome_generating_profile() -> None: + world = create_negotiation_world(3, "redaction-test", "free") + public = _public_world(world) + profile = public["project_risk_profiles"]["firm_risky"] + assert profile["redacted"] is True + assert "technology_maturity" not in profile + assert "technology_maturity" in profile["fields"] diff --git a/tests/test_negotiation.py b/tests/test_negotiation.py index 7c9e687..bff977a 100644 --- a/tests/test_negotiation.py +++ b/tests/test_negotiation.py @@ -2,12 +2,15 @@ import copy +import pytest + from insidegov.experiments import ( _pareto_frontier, run_negotiation_comparison, run_negotiation_matrix, ) from insidegov.models import NegotiationProtocol +from insidegov.negotiation_agents import ProposalAction from insidegov.negotiation_engine import COMPONENTS, NegotiationEngine from insidegov.scenarios import create_negotiation_world from insidegov.serde import world_from_dict @@ -25,6 +28,14 @@ def _accepted(world): return {r.firm_id: r for r in world.negotiation_records if r.outcome == "accepted"} +def test_llm_proposal_rejects_non_executable_tool_lists(): + with pytest.raises((TypeError, ValueError)): + ProposalAction.model_validate({ + "rationale": "列出政策工具", + "options": [{"label": "方案一", "tools": ["industry_fund", "tax_credit"]}], + }) + + def test_seven_protocols_are_distinct(): assert len(NegotiationProtocol) == 7 ids = [protocol.value for protocol in NegotiationProtocol] @@ -136,17 +147,28 @@ def test_gap_and_true_fit_are_bounded_and_consistent(): def test_low_quality_project_veto_path(): - # 高能电池(技术未成熟+无自筹):澄清充分的机制应识别并在签约前叫停 + # 政府不读取 unfeasible 标签;不同证据程序形成批准、试点、否决三种路径。 + free = _run(42, protocol="free") + free_case = next(c for c in free.due_diligence_cases if c.firm_id == "firm_risky") + assert free_case.decision == "approve" + assert free_case.uncertainty > 0.60 + clarify = _run(42, protocol="clarify_first") - risky = next(r for r in clarify.negotiation_records if r.firm_id == "firm_risky") + clarify_case = next(c for c in clarify.due_diligence_cases if c.firm_id == "firm_risky") + assert clarify_case.decision == "conditional_pilot" + assert clarify_case.estimated_failure_probability > free_case.estimated_failure_probability + + verified = _run(42, protocol="constraints_first") + verified_case = next(c for c in verified.due_diligence_cases if c.firm_id == "firm_risky") + risky = next(r for r in verified.negotiation_records if r.firm_id == "firm_risky") + assert verified_case.decision == "reject" assert risky.outcome == "terminated" - assert "低质项目" in (risky.fail_reason or "") - assert any("veto" in e.kind for e in clarify.events) - # 自由协商看不清真实投入 → 误签低质项目 - free = _run(42, protocol="free") - risky_free = next(r for r in free.negotiation_records if r.firm_id == "firm_risky") - assert risky_free.outcome == "accepted" - assert risky_free.policy_fit < 0.60 + assert "证据门控尽调否决" in (risky.fail_reason or "") + assert all( + audit.observation.get("hidden_quality_label_available") is False + for audit in verified.action_audits + if audit.action_type == "evidence_gated_due_diligence" + ) def test_phased_commitment_raises_incentive_alignment(): From 051ec241a76de4a4d222f9fd20073129f8416c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=BC=A0=E6=AC=A3?= <115392788+lizhangxin666@users.noreply.github.com> Date: Fri, 14 Aug 2026 00:23:00 +0800 Subject: [PATCH 2/7] feat(sim): add imitation exit and rescue dynamics --- src/insidegov/dynamic_competition.py | 200 +++++++++++ src/insidegov/engine.py | 483 +++++++++++++++++++++++++++ tests/test_dynamic_competition.py | 69 ++++ 3 files changed, 752 insertions(+) create mode 100644 src/insidegov/dynamic_competition.py create mode 100644 tests/test_dynamic_competition.py diff --git a/src/insidegov/dynamic_competition.py b/src/insidegov/dynamic_competition.py new file mode 100644 index 0000000..c11fce2 --- /dev/null +++ b/src/insidegov/dynamic_competition.py @@ -0,0 +1,200 @@ +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import asdict +from typing import Any + +from .engine import SimulationEngine +from .models import WorldState +from .scenarios import create_full_lifecycle_world + +POLICY_VARIANTS = ( + ( + "market_exit", + "允许市场退出", + "政府拒绝经营性输血,连续亏损企业退出并释放产能。", + ), + ( + "unconditional", + "持续无条件救助", + "政府优先稳就业,救助不附带压减产能要求。", + ), + ( + "conditional", + "附条件救助", + "政府提供有限救助,同时要求压减22%产能和12%岗位。", + ), +) + + +def run_dynamic_competition_experiment( + seed: int = 42, + quarters: int = 24, + demand_shock: float = -0.42, + policy_ids: list[str] | None = None, + mode: str = "deterministic", + model_name: str | None = None, + progress_callback: Callable[[dict[str, Any], object], None] | None = None, + cancel_check: Callable[[], bool] | None = None, + initial_world: WorldState | None = None, +) -> dict: + """Compare exit and rescue institutions under identical imitation pressure. + + All variants share the same seed, initial world, imitation regime and Q10 + demand shock. Only the rescue institution changes. + """ + selected = set(policy_ids or [item[0] for item in POLICY_VARIANTS]) + unknown = selected - {item[0] for item in POLICY_VARIANTS} + if unknown: + raise ValueError(f"unknown rescue policy: {', '.join(sorted(unknown))}") + variants: list[dict] = [] + for policy_id, name, description in POLICY_VARIANTS: + if policy_id not in selected: + continue + if initial_world is not None: + if len(selected) != 1: + raise ValueError("a resumed world can only continue one rescue policy") + world = initial_world + else: + world = create_full_lifecycle_world(seed, f"dynamic-{policy_id}-{seed}") + world.name = f"动态竞争实验 / {name}" + world.policy_mode = mode + world.model_name = model_name + world.imitation_policy = "aggressive" + world.rescue_policy = policy_id + engine = SimulationEngine(world) + if not any( + item.kind == "demand_shock" and item.target == "market" + for item in world.interventions + ): + engine.intervene("demand_shock", "market", demand_shock, quarter=10) + for quarter_index in range(world.quarter, quarters): + if cancel_check and cancel_check(): + raise InterruptedError("public job canceled") + before_actions = len(world.action_audits) + before_events = len(world.events) + engine.step() + if progress_callback: + progress_callback({ + "policy_id": policy_id, + "quarter": world.quarter, + "quarter_index": quarter_index + 1, + "quarters": quarters, + "new_audits": len(world.action_audits) - before_actions, + "new_events": [asdict(item) for item in world.events[before_events:]], + }, world) + final = world.history[-1] + trajectory = [ + { + "quarter": item.quarter, + "employment": item.total_employment, + "capacity": item.capacity, + "demand": item.demand, + "utilization": item.utilization, + "exited_firms": item.exited_firms, + "zombie_firms": item.zombie_firms, + "rescue_spending": item.rescue_spending, + "imitation_capacity": item.imitation_capacity, + } + for item in world.history + if item.quarter >= 8 + ] + variants.append({ + "id": policy_id, + "name": name, + "description": description, + "final": { + **asdict(final), + "phase": final.phase.value, + "available_budget": round( + sum(city.available_budget for city in world.cities.values()), 3 + ), + "rescue_spending": round( + sum(city.rescue_expenditure for city in world.cities.values()), 3 + ), + "imitation_projects": sum( + item.created_firm_id is not None + for item in world.imitation_decisions + ), + }, + "trajectory": trajectory, + "imitation_decisions": [ + asdict(item) for item in world.imitation_decisions + ], + "rescue_decisions": [ + asdict(item) for item in world.rescue_decisions + ], + "agent_runtime": { + "providers": sorted({ + item.provider for item in world.action_audits if item.provider + }), + "audited_agent_decisions": len(world.action_audits), + "fallback_count": sum(bool(item.fallback) for item in world.action_audits), + }, + "key_events": [ + asdict(item) for item in world.events + if item.quarter >= 9 and item.kind in { + "imitation", "market", "rescue", "exit_review", "exit", + "external_shock", + } + ][-24:], + }) + headline_comparison = {} + for variant in variants: + final = variant["final"] + if variant["id"] == "market_exit": + headline_comparison[variant["id"]] = ( + f"退出 {final['exited_firms']} 家,期末利用率" + f" {final['utilization']:.1%},救助支出为0。" + ) + elif variant["id"] == "unconditional": + headline_comparison[variant["id"]] = ( + f"保留就业 {final['total_employment']} 人,但形成" + f" {final['zombie_firms']} 家僵尸企业,累计救助" + f" {final['rescue_spending']:.1f} 亿元。" + ) + else: + headline_comparison[variant["id"]] = ( + f"保留就业 {final['total_employment']} 人,期末利用率" + f" {final['utilization']:.1%},未形成僵尸企业。" + ) + return { + "schema_version": "1.0", + "seed": seed, + "quarters": quarters, + "agent_runtime": { + "mode": mode, + "model_name": model_name, + "providers": sorted({ + provider for variant in variants + for provider in variant["agent_runtime"]["providers"] + }), + "audited_agent_decisions": sum( + variant["agent_runtime"]["audited_agent_decisions"] + for variant in variants + ), + "fallback_count": sum( + variant["agent_runtime"]["fallback_count"] for variant in variants + ), + "fixed_for_public_experience": mode == "llm", + }, + "common_conditions": { + "imitation_policy": "aggressive", + "demand_shock_quarter": 10, + "demand_shock": demand_shock, + "changed_variable": "rescue_policy", + }, + "causal_chain": [ + "中标城市产业项目投产并形成可观察成功信号", + "竞争城市招商局选择模仿,财政规则裁剪项目规模", + "同类产能集中进入,需求冲击后利用率与利润下降", + "企业申请救助,招商局、财政局和市领导分别表态", + "规则引擎执行退出、无条件救助或附条件重组", + "就业、财政支出、产能出清与僵尸企业出现分化", + ], + "variants": variants, + "headline_comparison": headline_comparison, + "interpretation_boundary": ( + "这是同一参数世界中的机制压力测试,用于比较制度路径,不代表现实政策效应大小。" + ), + } diff --git a/src/insidegov/engine.py b/src/insidegov/engine.py index d99ac26..5d00371 100644 --- a/src/insidegov/engine.py +++ b/src/insidegov/engine.py @@ -5,6 +5,7 @@ import random import uuid from dataclasses import asdict, is_dataclass +from typing import Any import httpx @@ -16,10 +17,14 @@ ) from .models import ( AgentActionAudit, + AgentRole, + AgentState, DecisionTrace, Event, ExternalNegotiationRound, FirmState, + FirmType, + ImitationDecision, Intervention, MemoryRecord, MetricsSnapshot, @@ -30,6 +35,7 @@ PolicyPackage, Promise, PromiseStatus, + RescueDecision, WorldState, ) from .organization import OrganizationProcessEngine @@ -76,7 +82,9 @@ def step(self) -> WorldState: self._settle_promises() if self.world.phase == Phase.INDUSTRIALIZATION: self._run_supplier_entry() + self._run_city_imitation() self._run_production() + self._run_distress_and_rescue() self._record_metrics() self.world.random_state = self.random.getstate() return self.world @@ -146,6 +154,32 @@ def _apply_interventions(self) -> None: f"{city.name}新增可用专项资金 {item.value:.1f} 亿元", city.id, severity="success", ) + elif item.kind == "project_progress_shock" and item.target in self.world.firms: + firm = self.world.firms[item.target] + before = firm.project_progress + firm.project_progress = min(1.0, max(0.0, before + item.value)) + self._event( + "external_shock", "企业建设延期", + f"{firm.name}项目进度由 {before:.0%} 调整为 {firm.project_progress:.0%}", + firm.id, severity="warning", + ) + elif item.kind == "credibility_diffusion_toggle": + enabled = item.value > 0.5 + self.world.mechanisms["credibility_diffusion"] = enabled + self._event( + "intervention", "信用传播机制调整", + "政府履约信号恢复向供应链传播" if enabled else "政府履约信息不再向外围供应商扩散", + item.target or None, severity="info" if enabled else "warning", + ) + elif item.kind == "superior_policy_support" and item.target in self.world.cities: + city = self.world.cities[item.target] + city.available_budget += max(0.0, item.value) + city.administrative_capacity = min(100.0, city.administrative_capacity + 3.0) + self._event( + "intervention", "上级产业政策窗口开启", + f"{city.name}获得 {item.value:.1f} 亿元专项支持并提高项目协调优先级", + city.id, severity="success", + ) def _run_recruitment(self) -> None: anchor = self.world.firms["firm_nova"] @@ -916,6 +950,219 @@ def _run_supplier_entry(self) -> None: {"jobs": float(firm.jobs_capacity)}, f"进入{city.name}供应链园区") self._event("entry", f"{firm.name}进入园区", "龙头订单与集聚效应提高了本地投资价值", firm.id, city.id, "success") + def _run_city_imitation(self) -> None: + """Let rival governments observe success and decide whether to imitate it. + + The organization agent selects a strategy. Land, fiscal cost and created + capacity are still checked and settled deterministically. + """ + if not self.world.mechanisms.get("city_imitation", True): + return + if self.world.quarter not in {10, 12, 14, 16, 18, 20}: + return + source_id = self.world.selected_city_id + if source_id is None: + return + source = self.world.cities[source_id] + anchor = self.world.firms.get("firm_nova") + if anchor is None or not anchor.operating: + return + source_signal = { + "operating_firms": float(sum( + firm.operating and firm.location == source_id + for firm in self.world.firms.values() + )), + "employment": float(source.employment), + "supply_chain": source.supply_chain, + "tax_revenue": source.tax_revenue, + "credibility": source.objective_credibility, + } + for city_id, city in self.world.cities.items(): + if city_id == source_id: + continue + previous = [ + item for item in self.world.imitation_decisions + if item.city_id == city_id and item.created_firm_id + ] + if previous and self.world.imitation_policy != "aggressive": + continue + investment = self.world.agents[f"{city_id}_investment"] + competitive_intensity = float( + investment.private_facts.get("competitive_intensity", 0.65) + ) + fiscal_space = max(0.0, 1 - city.fiscal_pressure) + success = min(1.0, ( + source_signal["operating_firms"] / 8 * 0.32 + + source_signal["supply_chain"] / 100 * 0.26 + + source_signal["employment"] / 5000 * 0.22 + + source_signal["credibility"] * 0.20 + )) + imitation_score = ( + success * 0.42 + + competitive_intensity * 0.30 + + fiscal_space * 0.18 + + city.gdp_weight * 0.10 + ) + if self.world.imitation_policy == "off": + candidates = ["watch_only"] + elif self.world.imitation_policy == "aggressive": + candidates = ["aggressive_imitation", "targeted_imitation", "watch_only"] + elif imitation_score >= 0.64: + candidates = ["targeted_imitation", "watch_only", "aggressive_imitation"] + else: + candidates = ["watch_only", "targeted_imitation", "aggressive_imitation"] + observation = { + "source_city": source.name, + "source_success": round(success, 3), + "imitation_score": round(imitation_score, 3), + "available_budget": round(city.available_budget, 3), + "fiscal_pressure": round(city.fiscal_pressure, 3), + "industrial_land": round(city.industrial_land, 3), + "existing_imitation_projects": len(previous), + } + memories = self._retrieve_memories(investment, "同类城市 招商 模仿 产能 风险") + choice = self._cognitive_call( + "choose_organization_action", investment, candidates, observation, memories, + ) + meta = self._last_cognition_meta.copy() + strategy = choice.action_id if choice.action_id in candidates else candidates[0] + requested_cost = 0.0 + requested_capacity = 0.0 + requested_jobs = 0 + land_need = 0.0 + if strategy == "targeted_imitation": + requested_cost = 4.8 + competitive_intensity * 2.4 + requested_capacity = 34.0 + competitive_intensity * 8.0 + requested_jobs = int(320 + city.employment_weight * 280) + land_need = 52.0 + elif strategy == "aggressive_imitation": + requested_cost = 8.5 + competitive_intensity * 4.5 + requested_capacity = 55.0 + competitive_intensity * 12.0 + requested_jobs = int(480 + city.employment_weight * 420) + land_need = 78.0 + finance = self.world.agents[f"{city_id}_finance"] + reserve = float(finance.private_facts.get("reserve_floor", city.available_budget * 0.34)) + spendable = max(0.0, city.available_budget - reserve) + hard_limit = min(spendable * 0.08, city.available_budget * 0.04) + land_ratio = min(1.0, city.industrial_land / max(land_need, 1.0)) + cost_ratio = min(1.0, hard_limit / max(requested_cost, 0.001)) + approved_ratio = min(land_ratio, cost_ratio) + approved_cost = round(requested_cost * approved_ratio, 2) + added_capacity = round(requested_capacity * approved_ratio, 2) + added_jobs = int(requested_jobs * approved_ratio) + created_firm_id: str | None = None + rule_adjustment = { + "fiscal_hard_limit": round(hard_limit, 2), + "land_available": round(city.industrial_land, 2), + "requested_cost": round(requested_cost, 2), + "approved_ratio": round(approved_ratio, 3), + } + # A city may authorize a smaller pilot rather than treating a fiscal + # haircut as an all-or-nothing veto. Below 18% the proposal is no + # longer a viable production line and is blocked. + if strategy != "watch_only" and approved_ratio >= 0.18: + created_firm_id = f"imitator_{city_id}_{self.world.quarter}" + firm = FirmState( + id=created_firm_id, + name=f"{city.name}同类项目{self.world.quarter}", + firm_type=FirmType.OPPORTUNISTIC, + investment_capacity=round(added_capacity * 1.4, 2), + cash=round(max(6.0, added_capacity * 0.24), 2), + land_need=round(land_need * approved_ratio, 2), + jobs_capacity=added_jobs, + production_capacity=added_capacity, + technology=58.0, + policy_sensitivity=0.78, + cluster_sensitivity=0.42, + credibility_sensitivity=0.48, + risk_tolerance=0.60, + private_intent=0.64, + minimum_utility=55.0, + location=city_id, + project_progress=1.0, + operating=True, + perceived_credibility={ + key: item.objective_credibility + for key, item in self.world.cities.items() + }, + ) + self.world.firms[created_firm_id] = firm + self.world.agents[f"{created_firm_id}_board"] = AgentState( + id=f"{created_firm_id}_board", + name=f"{firm.name}董事会", + role=AgentRole.ENTERPRISE, + owner_id=created_firm_id, + goals=["维持现金流", "争取地方支持", "控制退出损失"], + private_facts={ + "true_intent": firm.private_intent, + "minimum_cash": round(firm.cash * 0.35, 2), + }, + traits={ + "risk_aversion": 0.46, + "short_termism": 0.72, + "trust_sensitivity": 0.48, + }, + ) + city.available_budget -= approved_cost + city.imitation_expenditure += approved_cost + city.imitation_capacity += added_capacity + city.industrial_land -= firm.land_need + city.employment += added_jobs + city.landed_firms.append(created_firm_id) + self._event( + "imitation", f"{city.name}模仿招商形成同类项目", + f"观察到{source.name}项目投产后,新增产能 {added_capacity:.1f}、岗位 {added_jobs};" + f"财政投入 {approved_cost:.1f} 亿元。", + investment.id, created_firm_id, "warning", + ) + outcome = "规则引擎核准土地和财政后,同类项目投产" + else: + strategy = "watch_only" if strategy == "watch_only" else "blocked" + approved_cost = added_capacity = 0.0 + added_jobs = 0 + outcome = "继续观察" if strategy == "watch_only" else "财政或土地约束阻止模仿项目" + decision = ImitationDecision( + id=f"imitation-{len(self.world.imitation_decisions)+1:04d}", + quarter=self.world.quarter, + city_id=city_id, + source_city_id=source_id, + observed_signal={key: round(value, 3) for key, value in source_signal.items()}, + strategy=strategy, + requested_cost=round(requested_cost, 2), + approved_cost=approved_cost, + added_capacity=added_capacity, + added_jobs=added_jobs, + created_firm_id=created_firm_id, + rationale=choice.rationale, + provider=self.cognition.model_name or self.cognition.mode, + turns=[{ + "actor_id": investment.id, + "act": strategy, + "summary": choice.rationale, + }, { + "actor_id": finance.id, + "act": "hard_limit_review", + "summary": f"财政硬上限 {hard_limit:.2f} 亿元", + }], + ) + self.world.imitation_decisions.append(decision) + reflection = self.cognition.reflect(investment, strategy, outcome) + self._append_action_audit( + investment, "city_imitation", observation, memories, + self._action_dict(choice), { + "strategy": strategy, + "approved_cost": approved_cost, + "added_capacity": added_capacity, + "added_jobs": added_jobs, + "created_firm_id": created_firm_id, + }, rule_adjustment, choice.rationale, reflection, meta, + outcome="executed" if created_firm_id else strategy, + ) + self._remember( + investment, "city_imitation", f"{outcome};策略={strategy}", + 0.78, [decision.id], 0.2 if created_firm_id else -0.1, + ) + def _run_production(self) -> None: operating = [firm for firm in self.world.firms.values() if firm.operating] total_capacity = sum(firm.production_capacity for firm in operating) @@ -926,6 +1173,7 @@ def _run_production(self) -> None: revenue = firm.production_capacity * utilization * self.world.market_price cost = firm.production_capacity * (0.54 - min(0.12, len(operating) * 0.006)) firm.profit = revenue - cost + firm.cash = max(-20.0, firm.cash + firm.profit * 0.18) if firm.location: city = self.world.cities[firm.location] tax = max(0.0, firm.profit * 0.16) @@ -935,6 +1183,205 @@ def _run_production(self) -> None: if utilization < 0.68: self._event("market", "产能利用率预警", f"行业利用率降至 {utilization:.0%},地方政府面临救助压力", severity="warning") + def _run_distress_and_rescue(self) -> None: + """Detect persistent distress, convene government agents and settle outcomes.""" + for firm in list(self.world.firms.values()): + if not firm.operating or not firm.location: + continue + loss_signal = firm.profit < 0 or firm.utilization < 0.46 + if loss_signal: + firm.consecutive_losses += 1 + firm.distress_quarters += 1 + else: + firm.consecutive_losses = 0 + if firm.lifecycle_status in {"distressed", "restructured"}: + firm.lifecycle_status = "active" + if firm.consecutive_losses < 2: + continue + firm.lifecycle_status = "distressed" if firm.lifecycle_status != "zombie" else "zombie" + city = self.world.cities[firm.location] + board_id = f"{firm.id}_board" + if board_id not in self.world.agents: + self.world.agents[board_id] = AgentState( + id=board_id, name=f"{firm.name}董事会", role=AgentRole.ENTERPRISE, + owner_id=firm.id, goals=["恢复现金流", "维持经营", "控制退出损失"], + private_facts={"minimum_cash": max(1.0, firm.production_capacity * 0.06)}, + traits={"risk_aversion": 0.58, "short_termism": 0.52, "trust_sensitivity": 0.60}, + ) + board = self.world.agents[board_id] + investment = self.world.agents[f"{city.id}_investment"] + finance = self.world.agents[f"{city.id}_finance"] + leader = self.world.agents[f"{city.id}_leader"] + requested = round(max(1.0, -firm.profit * 0.42 + firm.production_capacity * 0.025), 2) + reserve = float(finance.private_facts.get("reserve_floor", city.available_budget * 0.34)) + spendable = max(0.0, city.available_budget - reserve) + finance_limit = round(min(requested, spendable * 0.07, city.available_budget * 0.035), 2) + observation = { + "firm": firm.name, + "firm_type": firm.firm_type.value, + "consecutive_losses": firm.consecutive_losses, + "distress_quarters": firm.distress_quarters, + "cash": round(firm.cash, 3), + "profit": round(firm.profit, 3), + "utilization": round(firm.utilization, 3), + "jobs": firm.jobs_capacity, + "capacity": round(firm.production_capacity, 3), + "city_fiscal_pressure": round(city.fiscal_pressure, 3), + "requested_amount": requested, + } + policy = self.world.rescue_policy + rescue_enabled = self.world.mechanisms.get("government_rescue", True) + if not rescue_enabled or policy == "market_exit": + leader_candidates = ["reject_rescue"] + elif policy == "unconditional": + leader_candidates = ["unconditional_rescue", "reject_rescue"] + elif policy == "conditional": + leader_candidates = ["conditional_rescue", "reject_rescue"] + else: + employment_stake = min(1.0, firm.jobs_capacity / 1800) + systemic = firm.firm_type == FirmType.ANCHOR or firm.supplier_of is not None + if finance_limit < requested * 0.45 or firm.rescue_count >= 2: + leader_candidates = ["reject_rescue", "conditional_rescue"] + elif systemic or employment_stake > 0.35: + leader_candidates = ["conditional_rescue", "unconditional_rescue", "reject_rescue"] + else: + leader_candidates = ["reject_rescue", "conditional_rescue"] + investment_candidates = ( + ["recommend_conditional_rescue", "recommend_exit"] + if firm.jobs_capacity >= 300 or firm.firm_type == FirmType.ANCHOR + else ["recommend_exit", "recommend_conditional_rescue"] + ) + finance_candidates = ( + ["approve_with_limit", "reject_fiscal_request"] + if finance_limit > 0 + else ["reject_fiscal_request"] + ) + turns: list[dict[str, Any]] = [] + audit_inputs: list[tuple] = [] + for actor, candidates, query in ( + (board, ["request_rescue", "voluntary_restructure", "orderly_exit"], "亏损 救助 退出"), + (investment, investment_candidates, "就业 救助 产业链"), + (finance, finance_candidates, "财政 救助 底线"), + (leader, leader_candidates, "救助 退出 就业 效率"), + ): + memories = self._retrieve_memories(actor, query) + choice = self._cognitive_call( + "choose_organization_action", actor, candidates, observation, memories, + ) + meta = self._last_cognition_meta.copy() + action_id = choice.action_id if choice.action_id in candidates else candidates[0] + turns.append({ + "actor_id": actor.id, "act": action_id, "summary": choice.rationale, + }) + audit_inputs.append((actor, memories, choice, meta, action_id)) + decision = turns[-1]["act"] + approved = 0.0 + conditional = decision == "conditional_rescue" + before_capacity = firm.production_capacity + before_jobs = firm.jobs_capacity + if decision in {"conditional_rescue", "unconditional_rescue"} and finance_limit > 0: + approved = finance_limit + city.available_budget -= approved + city.rescue_expenditure += approved + firm.cash += approved + firm.rescue_count += 1 + firm.rescue_received += approved + if conditional: + firm.production_capacity = round(firm.production_capacity * 0.78, 2) + firm.jobs_capacity = max(1, int(firm.jobs_capacity * 0.88)) + city.employment = max(0, city.employment - (before_jobs - firm.jobs_capacity)) + firm.lifecycle_status = "restructured" + firm.consecutive_losses = 0 + result_text = "附条件救助:压减过剩产能并分担就业调整" + else: + firm.consecutive_losses = 0 + if firm.rescue_count >= 2 and firm.utilization < 0.55: + firm.lifecycle_status = "zombie" + result_text = "再次无条件输血,企业继续经营但被识别为僵尸企业" + else: + firm.lifecycle_status = "distressed" + result_text = "无条件救助维持原产能与就业" + else: + decision = "reject_rescue" + result_text = "财政未出资,企业继续承受市场出清压力" + if ( + self.world.mechanisms.get("enterprise_exit", True) + and firm.distress_quarters >= 3 + ): + self._exit_firm(firm, city, "连续亏损且救助申请未获通过") + result_text = "救助未获通过,企业有序退出并释放产能" + rescue = RescueDecision( + id=f"rescue-{len(self.world.rescue_decisions)+1:04d}", + quarter=self.world.quarter, + city_id=city.id, + firm_id=firm.id, + requested_amount=requested, + finance_limit=finance_limit, + decision=decision, + approved_amount=round(approved, 2), + conditional=conditional and approved > 0, + capacity_before=round(before_capacity, 2), + capacity_after=round(firm.production_capacity, 2), + jobs_before=before_jobs, + jobs_after=firm.jobs_capacity if firm.operating else 0, + rationale=turns[-1]["summary"], + provider=self.cognition.model_name or self.cognition.mode, + turns=turns, + ) + self.world.rescue_decisions.append(rescue) + self._event( + "rescue" if approved else "exit_review", + f"{firm.name}救助会商:{decision}", + f"申请 {requested:.1f} 亿元,财政硬上限 {finance_limit:.1f} 亿元。{result_text}", + leader.id, firm.id, + "success" if conditional and approved else "warning", + ) + adjustment = { + "requested_amount": requested, + "finance_hard_limit": finance_limit, + "approved_amount": round(approved, 2), + "capacity_before": round(before_capacity, 2), + "capacity_after": round(firm.production_capacity, 2), + "jobs_before": before_jobs, + "jobs_after": firm.jobs_capacity if firm.operating else 0, + } + for actor, memories, choice, meta, action_id in audit_inputs: + reflection = self.cognition.reflect(actor, action_id, result_text) + self._append_action_audit( + actor, "enterprise_rescue_and_exit", observation, memories, + self._action_dict(choice), { + "selected_action": action_id, + "final_decision": decision, + "approved_amount": round(approved, 2), + }, adjustment, choice.rationale, reflection, meta, + outcome=result_text, + ) + self._remember( + actor, "rescue_outcome", f"{firm.name}:{result_text}", + 0.86, [rescue.id], 0.2 if approved else -0.25, + ) + + def _exit_firm(self, firm: FirmState, city, reason: str) -> None: + jobs = firm.jobs_capacity + firm.operating = False + firm.lifecycle_status = "exited" + firm.exit_quarter = self.world.quarter + firm.utilization = 0.0 + city.employment = max(0, city.employment - jobs) + city.industrial_land += firm.land_need * 0.65 + if firm.id in city.landed_firms: + city.landed_firms.remove(firm.id) + if firm.supplier_of: + city.supply_chain = max(0.0, city.supply_chain - 1.1) + for promise in self.world.promises: + if promise.firm_id == firm.id and promise.status == PromiseStatus.PENDING: + promise.status = PromiseStatus.CANCELLED + self._event( + "exit", f"{firm.name}退出市场", + f"{reason};释放产能 {firm.production_capacity:.1f},减少岗位 {jobs}。", + firm.id, city.id, "danger", + ) + def _update_market(self) -> None: trend = 100 + self.world.quarter * 3.4 cycle = math.sin((self.world.quarter + self.world.seed % 7) / 2.4) * 11 @@ -953,6 +1400,23 @@ def _record_metrics(self) -> None: average_credibility=round(sum(city.objective_credibility for city in self.world.cities.values()) / len(self.world.cities), 4), cluster_size=len(operating), capacity=round(capacity, 3), demand=round(self.world.market_demand, 3), utilization=round(utilization, 4), market_price=round(self.world.market_price, 4), + distressed_firms=sum( + firm.lifecycle_status in {"distressed", "restructured"} + for firm in self.world.firms.values() + ), + rescued_firms=sum(firm.rescue_count > 0 for firm in self.world.firms.values()), + exited_firms=sum( + firm.lifecycle_status == "exited" for firm in self.world.firms.values() + ), + zombie_firms=sum( + firm.lifecycle_status == "zombie" for firm in self.world.firms.values() + ), + rescue_spending=round( + sum(city.rescue_expenditure for city in self.world.cities.values()), 3 + ), + imitation_capacity=round( + sum(city.imitation_capacity for city in self.world.cities.values()), 3 + ), )) def _leader_observation(self, city, anchor) -> dict: @@ -1235,6 +1699,25 @@ def _select_organization_initiative( candidates: list[str], observation: dict[str, float | bool], ) -> tuple[str, str, float, str | None, str, str, bool]: + directive = next(( + item for item in self.world.experience_directives + if item.get("actor_id") == actor_id + and item.get("status") == "pending" + and int(item.get("execute_quarter", self.world.quarter)) <= self.world.quarter + ), None) + if directive is not None: + requested = str(directive.get("action_id", "")) + directive["resolved_quarter"] = self.world.quarter + if requested in candidates: + directive["status"] = "selected" + return ( + "act", requested, float(directive.get("urgency", 0.92)), + directive.get("target_actor_id"), + str(directive.get("statement") or "玩家角色主动发起该组织行动"), + "first_person_player", False, + ) + directive["status"] = "blocked" + directive["blocked_reason"] = "行动不属于本角色当前可执行权限或程序场域" actor = self.world.agents[actor_id] choice = self._cognitive_call( "choose_organization_initiative", diff --git a/tests/test_dynamic_competition.py b/tests/test_dynamic_competition.py new file mode 100644 index 0000000..f0c9f5d --- /dev/null +++ b/tests/test_dynamic_competition.py @@ -0,0 +1,69 @@ +from insidegov.dynamic_competition import run_dynamic_competition_experiment +from insidegov.engine import SimulationEngine +from insidegov.scenarios import create_full_lifecycle_world +from insidegov.serde import world_from_dict + + +def test_city_imitation_is_agent_selected_but_rule_bounded() -> None: + world = create_full_lifecycle_world(42, "imitation-test") + world.imitation_policy = "aggressive" + SimulationEngine(world).run(10) + + created = [item for item in world.imitation_decisions if item.created_firm_id] + assert created + assert all(item.approved_cost <= item.requested_cost for item in created) + assert all(item.added_capacity > 0 for item in created) + assert any(audit.action_type == "city_imitation" for audit in world.action_audits) + + +def test_exit_rescue_and_conditional_restructuring_diverge() -> None: + report = run_dynamic_competition_experiment(seed=42, quarters=24) + variants = {item["id"]: item for item in report["variants"]} + + assert variants["market_exit"]["final"]["exited_firms"] > 0 + assert variants["market_exit"]["final"]["rescue_spending"] == 0 + assert variants["unconditional"]["final"]["zombie_firms"] > 0 + assert variants["unconditional"]["final"]["rescue_spending"] > 0 + assert variants["conditional"]["final"]["zombie_firms"] == 0 + assert any( + item["conditional"] + and item["capacity_after"] < item["capacity_before"] + for item in variants["conditional"]["rescue_decisions"] + ) + + +def test_dynamic_state_round_trips() -> None: + world = create_full_lifecycle_world(42, "roundtrip-dynamic") + world.imitation_policy = "aggressive" + world.rescue_policy = "conditional" + engine = SimulationEngine(world) + engine.intervene("demand_shock", "market", -0.42, quarter=10) + engine.run(14) + + restored = world_from_dict(world.to_dict()) + assert restored.imitation_policy == "aggressive" + assert restored.rescue_policy == "conditional" + assert len(restored.imitation_decisions) == len(world.imitation_decisions) + assert len(restored.rescue_decisions) == len(world.rescue_decisions) + assert restored.history[-1].imitation_capacity == world.history[-1].imitation_capacity + + +def test_dynamic_experiment_resumes_without_replaying_saved_quarters() -> None: + world = create_full_lifecycle_world(31, "resume-dynamic") + world.imitation_policy = "aggressive" + world.rescue_policy = "conditional" + engine = SimulationEngine(world) + engine.intervene("demand_shock", "market", -0.42, quarter=10) + engine.run(12) + saved_history = list(world.history) + + report = run_dynamic_competition_experiment( + seed=31, + quarters=14, + policy_ids=["conditional"], + initial_world=world, + ) + + assert world.quarter == 14 + assert world.history[:len(saved_history)] == saved_history + assert report["variants"][0]["final"]["quarter"] == 14 From d98c8a7e0871810cdf9f99d84c6a78691dd74c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=BC=A0=E6=AC=A3?= <115392788+lizhangxin666@users.noreply.github.com> Date: Fri, 14 Aug 2026 00:23:55 +0800 Subject: [PATCH 3/7] feat(public): add problem-first LLM experiences --- src/insidegov/experience.py | 460 ++++++++++++++++++++++++++++++++++ src/insidegov/organization.py | 38 +++ src/insidegov/roadshow.py | 204 +++++++++++++++ tests/test_experience.py | 105 ++++++++ 4 files changed, 807 insertions(+) create mode 100644 src/insidegov/experience.py create mode 100644 src/insidegov/roadshow.py create mode 100644 tests/test_experience.py diff --git a/src/insidegov/experience.py b/src/insidegov/experience.py new file mode 100644 index 0000000..c9535de --- /dev/null +++ b/src/insidegov/experience.py @@ -0,0 +1,460 @@ +"""Public experience adapters backed by the authoritative InsideGov world. + +This module clean-room implements two presentation capabilities inspired by +the supplied G-E conversation and Novel prototypes: + +* compositional negotiation protocols and pressure-event visibility; +* first-person, game-like organization turns. + +Neither adapter owns a second simulation state. Conversation runs are normal +``NegotiationEngine`` worlds. Story sessions point to a normal +``SimulationEngine`` branch and submit role-checked directives into it. +""" + +from __future__ import annotations + +import json +import os +import uuid +from dataclasses import asdict, dataclass +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from .engine import SimulationEngine +from .models import AgentRole, Event, Phase, WorldState +from .negotiation_agents import CONVERSATION_MECHANISMS +from .negotiation_engine import NegotiationEngine +from .organization import playable_actions +from .repository import WorldRepository +from .scenarios import create_full_lifecycle_world, create_negotiation_world + +DIMENSION_LABELS = { + "mutual_confirmation": "双方确认需求", + "pre_commitment_consultation": "承诺前会商", + "conditional_commitment": "书面附条件承诺", +} + + +def _agent_runtime(world: WorldState) -> dict[str, Any]: + providers = sorted({item.provider for item in world.action_audits if item.provider}) + return { + "mode": world.policy_mode, + "model_name": world.model_name, + "providers": providers, + "audited_agent_decisions": len(world.action_audits), + "fallback_count": sum(bool(item.fallback) for item in world.action_audits), + "fixed_for_public_experience": world.policy_mode == "llm", + } + + +def conversation_mechanism_catalog() -> list[dict[str, Any]]: + results = [] + for mechanism_id, dimensions in CONVERSATION_MECHANISMS.items(): + enabled = [DIMENSION_LABELS[key] for key, value in dimensions.items() if value] + disabled = [DIMENSION_LABELS[key] for key, value in dimensions.items() if not value] + results.append({ + "id": mechanism_id, + "name": "+".join(enabled) if enabled else "直接受理与模糊承诺", + "dimensions": dict(dimensions), + "enabled": enabled, + "disabled": disabled, + "description": ";".join(enabled) if enabled else "不强制追问、前置会商或条件承诺", + }) + return results + + +def compile_pressure_event(text: str, firm_id: str = "firm_risky") -> dict[str, Any]: + """Compile a user condition into a transparent, bounded world change. + + The compiler intentionally supports a small, auditable vocabulary. It + never asks an LLM to invent a number and always exposes the applied demo + assumption to the user before results are interpreted. + """ + normalized = text.strip() or "企业融资计划反复调整,但尚未公开说明自筹资金缺口。" + if any(word in normalized for word in ("融资", "自筹", "资金链", "出资")): + event_type = "financing_stress" + title = "企业融资能力出现疑点" + change = {"target": f"{firm_id}.financing_capacity", "operation": "add", "value": -0.18} + informed = [f"{firm_id}_board"] + signals = ["融资计划调整", "自筹资金证明仍待补充"] + elif any(word in normalized for word in ("客户", "订单", "市场", "销量")): + event_type = "market_validation_stress" + title = "客户与订单验证出现疑点" + change = {"target": f"{firm_id}.market_validation", "operation": "add", "value": -0.16} + informed = [f"{firm_id}_board"] + signals = ["客户名单口径发生变化", "订单证明有待核验"] + elif any(word in normalized for word in ("诉讼", "实控人", "治理", "信用")): + event_type = "governance_stress" + title = "企业治理与信用出现疑点" + change = {"target": f"{firm_id}.governance_reliability", "operation": "add", "value": -0.15} + informed = [f"{firm_id}_board", "city_qing_legal"] + signals = ["公开登记信息发生变化", "治理材料需要复核"] + elif any(word in normalized for word in ("财政", "预算", "债务")): + event_type = "fiscal_tightening" + title = "地方财政空间收紧" + change = {"target": "city_qing.available_budget", "operation": "multiply", "value": 0.82} + informed = ["city_qing_finance"] + signals = ["预算安排收紧", "大额支持需要重新测算"] + else: + event_type = "information_signal" + title = "项目出现新的不确定信息" + change = {"target": "none", "operation": "observe", "value": 0.0} + informed = [f"{firm_id}_board"] + signals = [normalized[:80]] + all_agents = { + "city_qing_investment", "city_qing_finance", "city_qing_legal", + "city_qing_technical", f"{firm_id}_board", + } + return { + "event_type": event_type, + "title": title, + "source_text": normalized, + "affected_firm_id": firm_id, + "initially_informed_agent_ids": informed, + "initially_uninformed_agent_ids": sorted(all_agents - set(informed)), + "observable_signals": signals, + "authoritative_change": change, + "parameter_source": "用户条件+公开的Demo映射假设", + } + + +def _apply_pressure_event(world: WorldState, compiled: dict[str, Any]) -> None: + change = compiled["authoritative_change"] + target = str(change["target"]) + value = float(change["value"]) + firm_id = compiled["affected_firm_id"] + if target.endswith("financing_capacity"): + profile = world.project_risk_profiles[firm_id] + profile.financing_capacity = max(0.02, profile.financing_capacity + value) + elif target.endswith("market_validation"): + profile = world.project_risk_profiles[firm_id] + profile.market_validation = max(0.02, profile.market_validation + value) + elif target.endswith("governance_reliability"): + profile = world.project_risk_profiles[firm_id] + profile.governance_reliability = max(0.02, profile.governance_reliability + value) + elif target == "city_qing.available_budget": + world.cities["city_qing"].available_budget *= value + for agent_id in compiled["initially_informed_agent_ids"]: + if agent_id in world.agents: + world.agents[agent_id].private_facts["compiled_pressure_signal"] = compiled["source_text"] + world.events.append(Event( + quarter=0, + kind="compiled_pressure_event", + title=compiled["title"], + detail=( + f"{compiled['source_text']};初始知情方:" + f"{', '.join(compiled['initially_informed_agent_ids'])};" + f"可观察信号:{'、'.join(compiled['observable_signals'])}" + ), + actor_id=compiled["initially_informed_agent_ids"][0], + target_id=firm_id, + severity="warning", + )) + + +def run_conversation_experience( + repository: WorldRepository, + mechanism_id: str = "M8", + event_text: str = "企业融资计划反复调整,但尚未公开说明自筹资金缺口。", + seed: int = 42, + policy_mode: str = "deterministic", + model_name: str | None = None, +) -> tuple[WorldState, dict[str, Any]]: + mechanism_id = mechanism_id.upper() + catalog = {item["id"]: item for item in conversation_mechanism_catalog()} + if mechanism_id not in catalog: + raise ValueError(f"unknown conversation mechanism: {mechanism_id}") + world_id = f"conversation-{mechanism_id.lower()}-{uuid.uuid4().hex[:8]}" + world = create_negotiation_world( + seed=seed, world_id=world_id, protocol=mechanism_id, + language_style="plain", mode=policy_mode, + ) + world.name = f"协商压力测试 · {mechanism_id}" + world.model_name = model_name + compiled = compile_pressure_event(event_text) + # Public pressure tests follow one focal project. Keeping the unrelated + # seven calibration firms would multiply LLM calls without adding anything + # to the story the user asked to inspect. + focus_id = compiled["affected_firm_id"] + world.firms = {focus_id: world.firms[focus_id]} + world.latent_needs = {focus_id: world.latent_needs[focus_id]} + world.stated_needs = {focus_id: world.stated_needs[focus_id]} + world.gov_beliefs = {focus_id: world.gov_beliefs[focus_id]} + world.ent_beliefs = {focus_id: world.ent_beliefs[focus_id]} + world.project_risk_profiles = {focus_id: world.project_risk_profiles[focus_id]} + _apply_pressure_event(world, compiled) + engine = NegotiationEngine(world) + engine.step() + repository.save(engine.world) + repository.save_snapshot(engine.world) + + record = next(item for item in engine.world.negotiation_records if item.firm_id == focus_id) + diligence = next(item for item in engine.world.due_diligence_cases if item.firm_id == focus_id) + firm = engine.world.firms[focus_id] + timeline: list[dict[str, Any]] = [] + for item in diligence.agent_turns: + timeline.append({ + "round": item.get("round"), + "actor_id": item.get("actor_id", "city_qing_investment"), + "action": item.get("action_id", "尽调行动"), + "detail": item.get("rationale", "基于当前证据选择下一项核验"), + "kind": "evidence", + }) + for item in record.turns: + timeline.append({ + "round": item.get("round"), + "actor_id": item.get("actor", "city_qing_investment"), + "action": item.get("action", "协商行动"), + "detail": item.get("rationale") or item.get("text") or item.get("correction") or "形成新的协商状态", + "kind": "negotiation", + }) + report = { + "schema_version": "1.0", + "world_id": engine.world.id, + "authority_engine": "InsideGov.NegotiationEngine", + "agent_runtime": _agent_runtime(engine.world), + "mechanism": catalog[mechanism_id], + "compiled_event": compiled, + "focus_firm": {"id": firm.id, "name": firm.name}, + "result": { + "decision": diligence.decision, + "estimated_failure_probability": diligence.estimated_failure_probability, + "uncertainty": diligence.uncertainty, + "evidence_count": len(diligence.evidence), + "elapsed_days": diligence.elapsed_days, + "negotiation_outcome": record.outcome, + "fail_reason": record.fail_reason, + "understanding_gap_before": record.gap_initial, + "understanding_gap_after": record.gap_final, + "policy_fit": record.policy_fit, + "government_cost": record.gov_cost, + }, + "timeline": timeline, + "evidence": [asdict(item) for item in diligence.evidence], + "audit_count": len(engine.world.action_audits), + "boundary": "结果来自同一个InsideGov权威世界;事件数值为公开的Demo映射假设,不是现实企业评级。", + } + return engine.world, report + + +@dataclass(slots=True) +class StorySession: + id: str + source_world_id: str + world_id: str + player_agent_id: str + created_quarter: int + created_at: str + updated_at: str + turn_count: int = 0 + + +class StorySessionRepository: + def __init__(self, root: str | Path | None = None): + self.root = Path(root or os.getenv( + "INSIDEGOV_STORY_DIR", ".insidegov/story-sessions", + )) + self.root.mkdir(parents=True, exist_ok=True) + + def save(self, session: StorySession) -> None: + target = self.root / f"{session.id}.json" + temporary = target.with_suffix(".tmp") + temporary.write_text( + json.dumps(asdict(session), ensure_ascii=False, indent=2), encoding="utf-8", + ) + temporary.replace(target) + + def load(self, session_id: str) -> StorySession | None: + target = self.root / f"{session_id}.json" + if not target.exists(): + return None + return StorySession(**json.loads(target.read_text(encoding="utf-8"))) + + +STORY_ROLE_IDS = ( + "city_lin_investment", "city_lin_finance", "city_lin_leader", "city_lin_park", +) + + +def story_manifest() -> dict[str, Any]: + return { + "id": "insidegov-first-person", + "title": "坐进会场", + "subtitle": "同一个权威世界的第一人称组织博弈", + "roles": [ + {"id": "city_lin_investment", "name": "临江市招商局", "motive": "争取项目、抢占窗口"}, + {"id": "city_lin_finance", "name": "临江市财政局", "motive": "守住兑现能力与财政底线"}, + {"id": "city_lin_leader", "name": "临江市领导", "motive": "协调冲突并形成可执行方案"}, + {"id": "city_lin_park", "name": "临江市产业园区", "motive": "验证承载条件与产业配套"}, + ], + "rules": [ + "玩家只能选择角色权限内的行动。", + "其他组织仍由各自Agent自主行动。", + "财政、合同、项目与信用后果只由InsideGov规则引擎结算。", + "每局使用独立世界分支,不会改写原始世界。", + ], + } + + +def create_story_session( + repository: WorldRepository, + sessions: StorySessionRepository, + player_agent_id: str, + source_engine: SimulationEngine | None = None, + seed: int = 42, + policy_mode: str = "deterministic", + model_name: str | None = None, +) -> tuple[SimulationEngine, StorySession]: + if player_agent_id not in STORY_ROLE_IDS: + raise ValueError("该角色暂不属于第一人称体验的可选组织") + if source_engine is None: + source_id = f"story-source-{uuid.uuid4().hex[:8]}" + source_world = create_full_lifecycle_world(seed, source_id) + source_world.name = "第一人称招商会商" + source_world.policy_mode = policy_mode + source_world.model_name = model_name + source_engine = SimulationEngine(source_world) + repository.save(source_engine.world) + repository.save_snapshot(source_engine.world) + branch = source_engine.branch(f"story-world-{uuid.uuid4().hex[:8]}") + branch.world.name = f"坐进会场 · {branch.world.agents[player_agent_id].name}" + now = datetime.now(UTC).isoformat() + session = StorySession( + id=f"story-{uuid.uuid4().hex}", + source_world_id=source_engine.world.id, + world_id=branch.world.id, + player_agent_id=player_agent_id, + created_quarter=branch.world.quarter, + created_at=now, + updated_at=now, + ) + repository.save(branch.world) + repository.save_snapshot(branch.world) + sessions.save(session) + return branch, session + + +def _level(value: float, low: float, high: float) -> str: + return "承压" if value < low else "偏强" if value >= high else "不明朗" + + +def story_view(world: WorldState, session: StorySession, receipt: dict[str, Any] | None = None) -> dict[str, Any]: + player = world.agents[session.player_agent_id] + city = world.cities[player.owner_id] + city_actions = [ + item for item in world.organization_actions if item.city_id == player.owner_id + ][-10:] + player_action = next(( + item for item in reversed(city_actions) if item.actor_id == player.id + ), None) + recent_actions = ([player_action] if player_action else []) + [ + item for item in reversed(city_actions) + if player_action is None or item.id != player_action.id + ][:3] + recent_events = world.events[-6:] + private_facts = [ + {"key": key, "value": value} + for key, value in player.private_facts.items() + ] + if player.role != AgentRole.FINANCE: + private_facts = [item for item in private_facts if "reserve" not in item["key"]] + narrative = [] + for action in recent_actions: + narrative.append( + f"{world.agents.get(action.actor_id, player).name}选择“{action.action_name}”。" + f"{action.blocked_reason or action.selection_rationale or action.rationale}" + ) + if not narrative: + narrative = [ + "最终报价窗口正在缩短。招商、财政、园区和市领导都掌握着不同信息,尚无人知道会议会走向哪里。", + "你可以先试探边界、补充材料、推动议程,也可以选择一条更谨慎的路径。", + ] + return { + "session_id": session.id, + "source_world_id": session.source_world_id, + "world_id": world.id, + "authority_engine": "InsideGov.SimulationEngine", + "agent_runtime": _agent_runtime(world), + "turn": session.turn_count, + "quarter": world.quarter, + "phase": world.phase.value, + "scene_title": ( + "报价窗口前的会商" if world.phase == Phase.RECRUITMENT + else "承诺兑现进入倒计时" if world.phase == Phase.DELIVERY + else "产业链开始作出回应" + ), + "player": { + "id": player.id, "name": player.name, "role": player.role.value, + "goals": player.goals, "private_facts": private_facts, + "last_reflection": player.last_reflection, + }, + "signals": [ + {"label": "财政空间", "level": _level(1 - city.fiscal_pressure, 0.35, 0.7)}, + {"label": "组织共识", "level": _level(world.organization_processes[city.id].coalition_support, 0.4, 0.68)}, + {"label": "议程热度", "level": _level(world.organization_processes[city.id].agenda_priority, 0.35, 0.66)}, + {"label": "政府信用", "level": _level(city.objective_credibility, 0.62, 0.82)}, + ], + "narrative": narrative, + "recent_events": [asdict(item) for item in recent_events], + "available_actions": playable_actions(world, player.id), + "receipt": receipt, + "world_summary": { + "selected_city": world.selected_city_id, + "recruitment_status": world.recruitment_status, + "organization_actions": len(world.organization_actions), + "agent_audits": len(world.action_audits), + "events": len(world.events), + }, + "boundary": "这是权威世界的角色投影;你看不到其他组织未披露的私有信息。", + } + + +def take_story_turn( + engine: SimulationEngine, + session: StorySession, + sessions: StorySessionRepository, + repository: WorldRepository, + action_id: str, + statement: str, +) -> dict[str, Any]: + allowed = {item["id"] for item in playable_actions(engine.world, session.player_agent_id)} + if action_id not in allowed: + raise ValueError("该行动不属于角色权限或当前程序场域") + directive = { + "id": f"directive-{uuid.uuid4().hex[:10]}", + "actor_id": session.player_agent_id, + "action_id": action_id, + "statement": statement.strip() or "我决定现在发起这项行动。", + "urgency": 0.98, + "execute_quarter": engine.world.quarter + 1, + "status": "pending", + } + action_start = len(engine.world.organization_actions) + audit_start = len(engine.world.action_audits) + event_start = len(engine.world.events) + engine.world.experience_directives.append(directive) + engine.step() + new_actions = engine.world.organization_actions[action_start:] + executed = next((item for item in new_actions if item.actor_id == session.player_agent_id), None) + directive_status = next( + item for item in engine.world.experience_directives if item["id"] == directive["id"] + ) + if directive_status["status"] == "selected" and executed is not None: + directive_status["status"] = "executed" if executed.authorized else "blocked" + directive_status["blocked_reason"] = executed.blocked_reason + session.turn_count += 1 + session.updated_at = datetime.now(UTC).isoformat() + sessions.save(session) + repository.save(engine.world) + repository.save_snapshot(engine.world) + receipt = { + "directive_id": directive["id"], + "requested_action": action_id, + "status": directive_status["status"], + "executed_action": asdict(executed) if executed else None, + "new_agent_audits": len(engine.world.action_audits) - audit_start, + "new_events": [asdict(item) for item in engine.world.events[event_start:]], + "rule_statement": "行动先经角色权限和程序场域检查,财政与项目后果随后由规则引擎结算。", + } + return story_view(engine.world, session, receipt) diff --git a/src/insidegov/organization.py b/src/insidegov/organization.py index eba3fb5..f281bd3 100644 --- a/src/insidegov/organization.py +++ b/src/insidegov/organization.py @@ -79,6 +79,34 @@ class ActionDefinition: } +def playable_actions(world: WorldState, actor_id: str) -> list[dict[str, object]]: + """Return role- and arena-valid moves for a first-person experience. + + This is deliberately derived from the same catalog used by the authority + engine. The public UI therefore cannot invent a capability that the + underlying organization does not possess. + """ + actor = world.agents.get(actor_id) + if actor is None: + return [] + mode = ProcessMode(world.process_mode) + results = [] + for action_id in ROLE_ACTIONS.get(actor.role, ()): + definition = ACTION_CATALOG[action_id] + if action_id in {"fiscal_guardrail", "fund_due_diligence"}: + continue + if mode != ProcessMode.HYBRID and mode.value not in definition.arenas: + continue + results.append({ + "id": definition.id, + "name": definition.name, + "arena": " / ".join(definition.arenas), + "rationale": definition.rationale, + "evidence_ids": list(definition.evidence_ids), + }) + return results + + @dataclass(slots=True) class OrganizationRoundEffects: proposal_aggressiveness: float = 1.0 @@ -284,6 +312,16 @@ def _collect_initiatives( plan = self._ensure_plan(actor_id, city.id, allowed_actions, planning_observation) plan_node = self.dynamics.current_plan_node(plan) if plan else None candidates = [item for item in base_candidates if item in allowed_actions] + directive = next(( + item for item in self.world.experience_directives + if item.get("actor_id") == actor_id + and item.get("status") == "pending" + and int(item.get("execute_quarter", self.world.quarter)) <= self.world.quarter + ), None) + if directive and directive.get("action_id") in allowed_actions: + requested = str(directive["action_id"]) + if requested not in candidates: + candidates.append(requested) if mode != ProcessMode.FORMAL and "propose_open_action" not in candidates: candidates.append("propose_open_action") if plan_node and plan_node.action_id in allowed_actions and plan_node.action_id not in candidates: diff --git a/src/insidegov/roadshow.py b/src/insidegov/roadshow.py new file mode 100644 index 0000000..b2427aa --- /dev/null +++ b/src/insidegov/roadshow.py @@ -0,0 +1,204 @@ +"""Live roadshow workflows built on the source-backed Hefei–NIO world.""" + +from __future__ import annotations + +import uuid +from dataclasses import asdict +from typing import Any + +from .cases import create_hefei_nio_world +from .demo import create_hefei_nio_demo +from .engine import SimulationEngine +from .p2 import compare_worlds +from .repository import WorldRepository +from .serde import world_from_dict + +PRESSURE_SCENARIOS: dict[str, dict[str, Any]] = { + "fiscal_contraction": { + "name": "财政收缩", + "question": "如果Q4可用财力下降50%,已签承诺还能兑现吗?", + "interventions": [(4, "budget_multiply", "city_lin", 0.5)], + }, + "market_downturn": { + "name": "市场需求下降", + "question": "如果Q8市场需求下降35%,产能与财政回流会怎样?", + "interventions": [(8, "demand_shock", "market", -0.35)], + }, + "enterprise_delay": { + "name": "企业建设延期", + "question": "如果Q6企业项目进度倒退24个百分点,兑现节点会怎样变化?", + "interventions": [(6, "project_progress_shock", "firm_nova", -0.24)], + }, + "credit_interruption": { + "name": "信用传播中断", + "question": "如果政府履约信息无法传到外围供应商,集群还能形成吗?", + "interventions": [(4, "credibility_diffusion_toggle", "city_lin", 0.0)], + }, + "superior_policy": { + "name": "上级政策变化", + "question": "如果Q4新增15亿元专项支持并提高协调优先级,结果如何?", + "interventions": [(4, "superior_policy_support", "city_lin", 15.0)], + }, +} + + +TRAINING_CHOICES: dict[str, dict[str, dict[str, str]]] = { + "investment": { + "cash_push": {"name": "现金强攻", "description": "提高现金工具偏好,先争取签约再接受财政审查。"}, + "fund_coalition": {"name": "联合基金", "description": "优先组织多层级产业资本共同投资。"}, + "staged_pilot": {"name": "分阶段试点", "description": "降低首轮现金强度,以条件性承诺换取可执行性。"}, + }, + "finance": { + "strict_veto": {"name": "严格守底线", "description": "提高财政储备要求,优先避免跨期失信。"}, + "conditional": {"name": "有条件核准", "description": "保留当前底线,允许股权和分期替代现金。"}, + "development": {"name": "发展优先", "description": "降低储备要求,为重大项目释放更多空间。"}, + }, + "leader": { + "formal": {"name": "正式科层", "description": "按照送审、会签、协调和合同程序推进。"}, + "hybrid": {"name": "混合协调", "description": "正式程序与会前联盟、政策窗口共同发挥作用。"}, + "informal": {"name": "非正式推动", "description": "更多依赖注意力、联盟和机会窗口推动议程。"}, + }, +} + + +def create_roadshow_suite(repository: WorldRepository, seed: int = 42) -> dict[str, Any]: + """Generate consultation proof and five common-history stress branches.""" + consultation = create_hefei_nio_demo(repository, seed=seed, fiscal_multiplier=0.5) + root_id = f"roadshow-pressure-root-{uuid.uuid4().hex[:8]}" + root_engine = SimulationEngine(create_hefei_nio_world(seed, root_id)) + _save(repository, root_engine) + for _ in range(3): + root_engine.step() + _save(repository, root_engine) + + baseline = _clone_engine(root_engine, f"roadshow-pressure-baseline-{uuid.uuid4().hex[:8]}") + for _ in range(13): + baseline.step() + _save(repository, baseline) + + pressure_runs = [] + for scenario_id, scenario in PRESSURE_SCENARIOS.items(): + branch = _clone_engine(root_engine, f"roadshow-pressure-{scenario_id}-{uuid.uuid4().hex[:8]}") + branch.world.name = f"2020合肥—蔚来压力测试 / {scenario['name']}" + for quarter, kind, target, value in scenario["interventions"]: + branch.intervene(kind, target, value, quarter) + for _ in range(13): + branch.step() + _save(repository, branch) + comparison = compare_worlds(baseline.world, branch.world) + pressure_runs.append({ + "id": scenario_id, + "name": scenario["name"], + "question": scenario["question"], + "world": branch.world, + "summary": _world_summary(branch.world), + "comparison": comparison, + "interventions": [asdict(item) for item in branch.world.interventions], + "key_events": [asdict(item) for item in branch.world.events if item.quarter >= 4 and item.kind in {"intervention", "external_shock", "promise", "milestone", "market", "entry"}][-16:], + }) + + return { + "schema_version": "1.0", + "source": "live_deterministic_simulation", + "seed": seed, + "consultation": consultation, + "pressure": { + "common_ancestor_world_id": root_engine.world.id, + "common_ancestor_quarter": 3, + "baseline": baseline.world, + "baseline_summary": _world_summary(baseline.world), + "runs": pressure_runs, + }, + "training_choices": TRAINING_CHOICES, + } + + +def run_training_choice( + repository: WorldRepository, + role: str, + strategy: str, + seed: int = 42, +) -> dict[str, Any]: + if role not in TRAINING_CHOICES or strategy not in TRAINING_CHOICES[role]: + raise ValueError("unsupported training role or strategy") + world = create_hefei_nio_world(seed, f"roadshow-training-{role}-{strategy}-{uuid.uuid4().hex[:8]}") + choice = TRAINING_CHOICES[role][strategy] + _apply_training_choice(world, role, strategy) + world.name = f"干部协调训练 / {choice['name']}" + engine = SimulationEngine(world) + _save(repository, engine) + for _ in range(16): + engine.step() + _save(repository, engine) + final_meeting = next((item for item in reversed(engine.world.negotiations) if item.city_id == "city_lin"), None) + outward = next((item for item in reversed(engine.world.external_negotiations) if item.city_id == "city_lin"), None) + return { + "source": "live_deterministic_simulation", + "role": role, + "strategy": strategy, + "choice": choice, + "world": engine.world, + "short_term": { + "selected_city": engine.world.cities[engine.world.selected_city_id].name if engine.world.selected_city_id else None, + "enterprise_response": outward.enterprise_response if outward else "no_offer", + "utility": outward.utility if outward else None, + "minimum_utility": outward.minimum_utility if outward else None, + "finance_approved": final_meeting.finance_approved if final_meeting else None, + "proposal_cost": final_meeting.proposal_cost if final_meeting else None, + "final_cost": final_meeting.final_cost if final_meeting else None, + "conflicts": final_meeting.concerns if final_meeting else [], + "turns": final_meeting.turns if final_meeting else [], + }, + "long_term": _world_summary(engine.world), + "audit_count": len(engine.world.action_audits), + "organization_action_count": len(engine.world.organization_actions), + } + + +def _apply_training_choice(world, role: str, strategy: str) -> None: + if role == "investment": + facts = world.agents["city_lin_investment"].private_facts + if strategy == "cash_push": + facts.update({"cash_preference": 0.92, "competitive_intensity": 0.98, "external_fund_target": 20.0}) + elif strategy == "fund_coalition": + facts.update({"cash_preference": 0.22, "competitive_intensity": 0.88, "external_fund_target": 53.52}) + else: + facts.update({"cash_preference": 0.08, "competitive_intensity": 0.60, "external_fund_target": 35.0}) + elif role == "finance": + facts = world.agents["city_lin_finance"].private_facts + reserve = {"strict_veto": 125.0, "conditional": 86.0, "development": 58.0}[strategy] + facts["reserve_floor"] = reserve + elif role == "leader": + world.process_mode = strategy + + +def _clone_engine(root: SimulationEngine, world_id: str) -> SimulationEngine: + cloned = world_from_dict(root.world.to_dict()) + cloned.id = world_id + cloned.parent_id = root.world.id + cloned.branched_from_quarter = 3 + return SimulationEngine(cloned) + + +def _save(repository: WorldRepository, engine: SimulationEngine) -> None: + repository.save(engine.world) + repository.save_snapshot(engine.world) + + +def _world_summary(world) -> dict[str, Any]: + latest = world.history[-1] + selected = world.cities.get(world.selected_city_id) if world.selected_city_id else None + promises = [item for item in world.promises if item.city_id == world.selected_city_id] + return { + "quarter": world.quarter, + "selected_city": selected.name if selected else None, + "employment": latest.total_employment, + "cluster": latest.cluster_size, + "credibility": selected.objective_credibility if selected else latest.average_credibility, + "spending": latest.total_committed_expenditure, + "demand": latest.demand, + "utilization": latest.utilization, + "fulfilled_promises": sum(item.status.value == "fulfilled" for item in promises), + "delayed_promises": sum(item.status.value == "delayed" for item in promises), + "operating": world.firms["firm_nova"].operating, + } diff --git a/tests/test_experience.py b/tests/test_experience.py new file mode 100644 index 0000000..e3c5b3f --- /dev/null +++ b/tests/test_experience.py @@ -0,0 +1,105 @@ +from pathlib import Path + +from fastapi.testclient import TestClient + +from insidegov.api import app, repository, story_session_repository, worlds +from insidegov.engine import SimulationEngine +from insidegov.experience import ( + StorySessionRepository, + conversation_mechanism_catalog, + create_story_session, + run_conversation_experience, + story_view, + take_story_turn, +) +from insidegov.repository import WorldRepository + +client = TestClient(app) + + +def test_conversation_mechanisms_are_composed_and_use_authority_world(tmp_path): + catalog = conversation_mechanism_catalog() + assert [item["id"] for item in catalog] == [f"M{i}" for i in range(1, 9)] + assert len({tuple(item["dimensions"].values()) for item in catalog}) == 8 + + repo = WorldRepository(tmp_path / "worlds") + world, report = run_conversation_experience( + repo, "M8", "企业融资计划反复调整,自筹资金证明待补充。", seed=42, + ) + assert report["authority_engine"] == "InsideGov.NegotiationEngine" + assert report["world_id"] == world.id + assert repo.load(world.id) is not None + assert report["compiled_event"]["initially_informed_agent_ids"] + assert report["compiled_event"]["initially_uninformed_agent_ids"] + assert report["timeline"] + assert report["audit_count"] > 0 + assert world.project_risk_profiles["firm_risky"].financing_capacity < 0.3 + + +def test_first_person_turn_is_role_checked_and_settled_by_simulation_engine(tmp_path): + repo = WorldRepository(tmp_path / "worlds") + sessions = StorySessionRepository(tmp_path / "stories") + engine, session = create_story_session(repo, sessions, "city_lin_finance") + assert isinstance(engine, SimulationEngine) + before = story_view(engine.world, session) + allowed = {item["id"] for item in before["available_actions"]} + assert "risk_assessment" in allowed + assert "frame_strategic_project" not in allowed + + after = take_story_turn( + engine, session, sessions, repo, + "risk_assessment", "请先完成财政风险评估,再讨论报价。", + ) + assert after["quarter"] == 1 + assert after["receipt"]["status"] == "executed" + assert after["receipt"]["executed_action"]["action_id"] == "risk_assessment" + assert after["receipt"]["executed_action"]["selection_provider"] == "first_person_player" + assert after["receipt"]["new_agent_audits"] > 0 + assert "临江市财政局" in after["narrative"][0] + assert "财政风险评估" in after["narrative"][0] + assert repo.load(after["world_id"]) is not None + + +def test_public_experience_endpoints_require_llm_and_hide_mode_switch(tmp_path, monkeypatch): + monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False) + worlds.clear() + original_world_root = repository.root + original_story_root = story_session_repository.root + repository.root = Path(tmp_path) / "worlds" + repository.root.mkdir(parents=True, exist_ok=True) + story_session_repository.root = Path(tmp_path) / "stories" + story_session_repository.root.mkdir(parents=True, exist_ok=True) + try: + mechanisms = client.get("/experience/conversation-mechanisms") + assert mechanisms.status_code == 200 + assert len(mechanisms.json()) == 8 + conversation = client.post("/experience/conversations", json={ + "mechanism_id": "M8", + "event_text": "核心客户名单与订单口径发生变化。", + }) + assert conversation.status_code == 503 + assert "DEEPSEEK_API_KEY" in conversation.text + forbidden_switch = client.post("/experience/conversations", json={ + "mechanism_id": "M8", + "event_text": "核心客户名单与订单口径发生变化。", + "policy_mode": "deterministic", + }) + assert forbidden_switch.status_code == 422 + + manifest = client.get("/experience/story-manifest") + assert manifest.status_code == 200 + started = client.post("/experience/story-sessions", json={ + "player_agent_id": "city_lin_finance", + }) + assert started.status_code == 503 + for endpoint, payload in ( + ("/experience/coordination", {"process_mode": "hybrid"}), + ("/experience/due-diligence", {"program": "adaptive_staged"}), + ("/experience/dynamic-competition", {"policy": "conditional"}), + ): + response = client.post(endpoint, json=payload) + assert response.status_code == 503 + finally: + worlds.clear() + repository.root = original_world_root + story_session_repository.root = original_story_root From 05dec956b754d43543759916a5bd0eef6473e0d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=BC=A0=E6=AC=A3?= <115392788+lizhangxin666@users.noreply.github.com> Date: Fri, 14 Aug 2026 00:24:51 +0800 Subject: [PATCH 4/7] feat(jobs): add durable public runs and live progress --- apps/web/app/components/DueDiligenceLab.tsx | 109 ++++ .../app/components/DynamicCompetitionLab.tsx | 195 ++++++ apps/web/app/components/ExperienceHome.tsx | 489 +++++++++++++++ .../app/components/PublicJobExperience.tsx | 214 +++++++ apps/web/app/globals.css | 25 + apps/web/app/lib/api.ts | 444 ++++++++++++++ apps/web/app/page.tsx | 65 +- apps/web/tests/rendered-html.test.mjs | 4 +- src/insidegov/api.py | 580 +++++++++++++++++- src/insidegov/public_jobs.py | 241 ++++++++ src/insidegov/public_worker.py | 370 +++++++++++ tests/test_api.py | 37 +- tests/test_public_jobs.py | 76 +++ 13 files changed, 2838 insertions(+), 11 deletions(-) create mode 100644 apps/web/app/components/DueDiligenceLab.tsx create mode 100644 apps/web/app/components/DynamicCompetitionLab.tsx create mode 100644 apps/web/app/components/ExperienceHome.tsx create mode 100644 apps/web/app/components/PublicJobExperience.tsx create mode 100644 src/insidegov/public_jobs.py create mode 100644 src/insidegov/public_worker.py create mode 100644 tests/test_public_jobs.py diff --git a/apps/web/app/components/DueDiligenceLab.tsx b/apps/web/app/components/DueDiligenceLab.tsx new file mode 100644 index 0000000..e0401e7 --- /dev/null +++ b/apps/web/app/components/DueDiligenceLab.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { + api, + type DueDiligenceCase, + type DueDiligenceReport, +} from "../lib/api"; + +const decisions: Record = { + approve: "正式推进", + conditional_pilot: "可逆试点", + defer: "补证后再议", + reject: "签约前否决", +}; +const dimensionNames: Record = { + financing: "资金闭合", + technology: "技术成熟", + market: "市场验证", + governance: "治理信用", + execution: "交付能力", +}; + +export function DueDiligenceLab() { + const [report, setReport] = useState(null); + const [program, setProgram] = useState("adaptive_staged"); + const [seed, setSeed] = useState(3); + const [firmId, setFirmId] = useState("firm_risky"); + const [busy, setBusy] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + let alive = true; + void api.dueDiligence() + .then((value) => { + if (!alive) return; + setReport(value); + setSeed(value.configuration.seeds[0]); + }) + .catch((caught) => alive && setError(String(caught))) + .finally(() => alive && setBusy(false)); + return () => { alive = false; }; + }, []); + + const run = useMemo( + () => report?.program_runs.find((item) => item.program === program && item.seed === seed), + [program, report, seed], + ); + const current = useMemo( + () => run?.cases.find((item) => item.firm_id === firmId) ?? run?.cases[0], + [firmId, run], + ); + const contrast = useMemo(() => { + if (!report) return []; + return ["light_screen", "independent_verification", "adaptive_staged"].map((id) => { + const row = report.program_runs.find((item) => item.program === id && item.seed === seed); + return row?.cases.find((item) => item.firm_id === "firm_risky"); + }).filter((item): item is DueDiligenceCase => Boolean(item)); + }, [report, seed]); + + if (busy) return
EVIDENCE-GATED DUE DILIGENCE

正在运行 5 种程序 × 8 个随机种子…

; + if (error || !report) return

研究矩阵未完成

{error}

; + + return ( +
+
+
UNKNOWN FIRM · NO ORACLE LABEL

面对没有答案标签的新企业,什么程序更有识别力?

{report.research_question}

+ Agent 看不到真实质量 ✓ +
+ +
+ {report.program_summary.map((item) => ( + + ))} +
+ +
+

同一企业、同一 Seed,只改变证据程序

+
+ {contrast.map((item) =>
{report.program_summary.find((row) => row.id === item.program)?.name}{decisions[item.decision]}

事前风险 {pct(item.estimated_failure_probability)} · 不确定性 {pct(item.uncertainty)}

{item.evidence.length} 项证据 / {item.elapsed_days} 天 / 成本 {item.diligence_cost.toFixed(2)}
)} +
+
+ +
+
+

证据链

+ {current?.evidence.map((item) => ( +
+ R{item.round}
{item.requested_by} · {dimensionNames[item.dimension]}{item.source_type}

{item.summary}

可靠度{pct(item.reliability)}材料主张{pct(item.claim_value)}核验观察{pct(item.observed_quality)} +
+ ))} +
+
+

Agent 自主性与规则边界

+
    {current?.agent_turns.map((turn, index) =>
  1. {index + 1}{String(turn.actor_id)}{String(turn.action_id)}{String(turn.rationale ?? "基于风险与信息价值选择下一步")}
  2. )}
+ {current &&
FINAL DECISION{decisions[current.decision]}

规则引擎没有替 Agent 判断项目好坏;只负责证据可见性、权限、硬红线和事后结算。

decision_used_hidden_label = {String(current.decision_used_hidden_label)}
} +
+
+ +

阈值不是答案:它改变误放与误伤的取舍

{report.threshold_curve.map((item) =>
{item.threshold.toFixed(2)}抓到 {pct(item.true_positive_rate)}
误伤 {pct(item.false_positive_rate)}
)}
+

{report.interpretation_boundary}

+
+ ); +} + +function pct(value: number) { return `${(value * 100).toFixed(0)}%`; } diff --git a/apps/web/app/components/DynamicCompetitionLab.tsx b/apps/web/app/components/DynamicCompetitionLab.tsx new file mode 100644 index 0000000..c2b453e --- /dev/null +++ b/apps/web/app/components/DynamicCompetitionLab.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { + api, + type DynamicCompetitionReport, + type DynamicRescueDecision, +} from "../lib/api"; + +const decisionNames: Record = { + market_exit: "市场退出", + unconditional_rescue: "无条件救助", + conditional_rescue: "附条件救助", + unconditional: "无条件救助", + conditional: "附条件救助", + reject_rescue: "拒绝救助", + aggressive_imitation: "激进模仿", + targeted_imitation: "定向模仿", + blocked: "被硬约束拦截", +}; + +export function DynamicCompetitionLab() { + const [report, setReport] = useState(null); + const [selected, setSelected] = useState("conditional"); + const [busy, setBusy] = useState(true); + const [error, setError] = useState(""); + + async function run() { + setBusy(true); + setError(""); + try { + setReport(await api.dynamicCompetition()); + } catch (caught) { + setError(String(caught)); + } finally { + setBusy(false); + } + } + + useEffect(() => { + let alive = true; + void api + .dynamicCompetition() + .then((result) => { + if (alive) setReport(result); + }) + .catch((caught) => { + if (alive) setError(String(caught)); + }) + .finally(() => { + if (alive) setBusy(false); + }); + return () => { + alive = false; + }; + }, []); + + const current = useMemo( + () => report?.variants.find((item) => item.id === selected), + [report, selected], + ); + const sampleRescue = current?.rescue_decisions.find( + (item) => item.decision !== "reject_rescue", + ) ?? current?.rescue_decisions[0]; + + if (busy) + return ( +
+ DYNAMIC COMPETITION · SAME SEED +

正在运行三种退出与救助制度…

+

共同随机种子、共同城市模仿、共同 Q10 需求冲击,仅改变救助制度。

+
+ ); + if (error || !report) + return ( +
+ DYNAMIC COMPETITION +

实验未完成

+

{error}

+ +
+ ); + + return ( +
+
+
+ DYNAMIC COMPETITION · SEED {report.seed} +

城市模仿 → 产能过剩 → 企业退出与政府救助

+

+ Q10 需求下降 {Math.abs(report.common_conditions.demand_shock * 100).toFixed(0)}%, + 三个世界只改变救助制度。 +

+
+ 24 季度真实规则推演 +
+ +
    + {report.causal_chain.map((item, index) => ( +
  1. + {index + 1} + {item} +
  2. + ))} +
+ +
+ {report.variants.map((variant) => ( + + ))} +
+ + {current && ( +
+
+

{current.name}:季度演化

+
+ {current.trajectory.map((item) => ( +
+ + Q{item.quarter} +
+ ))} +
+
+ 模仿项目 {current.final.imitation_projects} + 模仿产能 {current.final.imitation_capacity.toFixed(1)} + 期末总产能 {current.final.capacity.toFixed(1)} + 期末利用率 {(current.final.utilization * 100).toFixed(1)}% +
+
+
+

一次真实救助会商

+ {sampleRescue ? :

该制度下没有发生救助会商。

} +
+
+ )} + +
+ {Object.entries(report.headline_comparison).map(([key, value]) => ( +

{decisionNames[key] ?? key}{value}

+ ))} +
+

{report.interpretation_boundary}

+
+ ); +} + +function RescueMeeting({ decision }: { decision: DynamicRescueDecision }) { + return ( +
+
+ 企业申请 {decision.requested_amount.toFixed(1)} 亿 + 财政硬上限 {decision.finance_limit.toFixed(1)} 亿 + 最终执行 {decision.approved_amount.toFixed(1)} 亿 +
+ {decision.turns.map((turn, index) => ( +
+ {turn.actor_id} + {decisionNames[turn.act ?? ""] ?? turn.act} +

{turn.summary}

+
+ ))} +
+ RULE ENGINE SETTLEMENT +

+ 产能 {decision.capacity_before.toFixed(1)} → {decision.capacity_after.toFixed(1)}; + 岗位 {decision.jobs_before} → {decision.jobs_after}。 +

+
+
+ ); +} diff --git a/apps/web/app/components/ExperienceHome.tsx b/apps/web/app/components/ExperienceHome.tsx new file mode 100644 index 0000000..a4548e5 --- /dev/null +++ b/apps/web/app/components/ExperienceHome.tsx @@ -0,0 +1,489 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { + api, + type ConversationExperience, + type ConversationMechanism, + type DueDiligenceCase, + type DueDiligenceReport, + type DynamicCompetitionReport, + type StoryManifest, + type StoryView, + type World, +} from "../lib/api"; +import { PublicJobRoom, PublicTaskDock } from "./PublicJobExperience"; + +type SceneId = "coordination" | "diligence" | "rescue" | "conversation" | "story"; +type Audience = "government" | "enterprise" | "public"; +type Outcome = { + headline: string; + conclusion: string; + metrics: Array<{ label: string; value: string }>; + process: Array<{ actor: string; action: string; feedback: string; tone?: string }>; + findings: string[]; + governmentActions: string[]; + enterpriseActions: string[]; + publicSummary: string; + story: string[]; + boundary: string; + runtime: { + modelName: string; + decisions: number; + fallbacks: number; + }; +}; + +const scenes = [ + { + id: "coordination" as const, + index: "01", + eyebrow: "会商前预演", + title: "怎样让部门冲突更早暴露?", + description: "让招商、财政、市领导和企业在会前先走一遍,查看分歧在哪里出现、怎样重组政策包。", + users: "招商干部 · 财政部门 · 项目企业", + time: "真实 Agent · 数分钟", + accent: "mint", + }, + { + id: "diligence" as const, + index: "02", + eyebrow: "项目真实性", + title: "一家新企业到底靠不靠谱?", + description: "不预先告诉 Agent 答案,让它选择查资金、技术、客户还是信用,并比较不同尽调程序。", + users: "招商部门 · 基金团队 · 企业", + time: "真实 Agent · 数分钟", + accent: "amber", + }, + { + id: "rescue" as const, + index: "03", + eyebrow: "产业压力测试", + title: "需求下滑时,应该救还是退?", + description: "在完全相同的冲击下,对比市场退出、无条件救助和附条件救助的就业与财政后果。", + users: "政府决策者 · 企业 · 社会公众", + time: "真实 Agent · 长时推演", + accent: "blue", + }, + { + id: "conversation" as const, + index: "04", + eyebrow: "协商机制压力测试", + title: "同一句诉求,怎样问才不容易误签?", + description: "输入一条项目异动,切换确认、会商和条件承诺机制,看信息如何暴露、承诺如何形成。", + users: "政府部门 · 项目企业 · 研究教学", + time: "真实 Agent · 数分钟", + accent: "rose", + }, + { + id: "story" as const, + index: "05", + eyebrow: "第一人称组织博弈", + title: "如果你坐进会场,会先做什么?", + description: "选择招商、财政、市领导或园区角色。你发起一项行动,其他组织自主回应,规则结算真实后果。", + users: "干部培训 · 普通公众 · 比赛展示", + time: "每回合由 Agent 实时生成", + accent: "violet", + }, +]; + +const strategies: Record> = { + coordination: [ + { id: "formal", name: "正式程序", note: "按送审、审核、集体决策依次推进" }, + { id: "informal", name: "非正式协调", note: "先通过会前沟通和联盟降低阻力" }, + { id: "hybrid", name: "混合模式", note: "非正式探测边界,再进入正式审查" }, + ], + diligence: [ + { id: "light_screen", name: "轻量筛查", note: "主要依赖企业提交材料,快且便宜" }, + { id: "independent_verification", name: "独立核验", note: "逐项验证资金、技术、市场和信用" }, + { id: "adaptive_staged", name: "自适应+试点", note: "按信息价值调查,风险项目先做可逆试点" }, + ], + rescue: [ + { id: "market_exit", name: "允许市场退出", note: "不提供救助,接受短期就业冲击" }, + { id: "unconditional", name: "无条件救助", note: "优先稳就业,但财政承担全部风险" }, + { id: "conditional", name: "附条件救助", note: "救助换整改,并压缩低效产能" }, + ], + conversation: [], + story: [], +}; + +export function ExperienceHome({ onOpenResearch }: { onOpenResearch: () => void }) { + const [scene, setScene] = useState(null); + const [strategy, setStrategy] = useState(""); + const [outcome, setOutcome] = useState(null); + const [audience, setAudience] = useState("government"); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + const [activeJobId, setActiveJobId] = useState(null); + const [conversationResult, setConversationResult] = useState(null); + const [storyResult, setStoryResult] = useState(null); + const selectedScene = useMemo(() => scenes.find((item) => item.id === scene), [scene]); + + function enter(id: SceneId) { + setScene(id); + setStrategy(id === "coordination" ? "hybrid" : id === "diligence" ? "adaptive_staged" : id === "rescue" ? "conditional" : ""); + setOutcome(null); + setConversationResult(null); + setStoryResult(null); + setAudience("government"); + window.scrollTo({ top: 0, behavior: "smooth" }); + } + + async function run() { + if (!scene || !strategy) return; + setBusy(true); + setError(""); + setOutcome(null); + try { + if (scene === "coordination") { + const job = await api.createPublicJob("coordination", { process_mode: strategy, seed: 42 }); + setActiveJobId(job.id); + } else if (scene === "diligence") { + const job = await api.createPublicJob("diligence", { program: strategy, seed: 42 }); + setActiveJobId(job.id); + } else if (scene === "rescue") { + const job = await api.createPublicJob("dynamic_competition", { policy: strategy, seed: 42, quarters: 16 }); + setActiveJobId(job.id); + } + } catch (caught) { + setError(String(caught)); + } finally { + setBusy(false); + } + } + + async function openJobResult(job: import("../lib/api").PublicJob) { + if (job.scene === "coordination") { + const result = await api.getPublicJobResult(job.id); + const mode = String(job.config.process_mode ?? "hybrid"); + setScene("coordination"); setStrategy(mode); setOutcome(coordinationOutcome(result, mode)); + } else if (job.scene === "diligence") { + const result = await api.getPublicJobResult(job.id); + const program = String(job.config.program ?? "adaptive_staged"); + setScene("diligence"); setStrategy(program); setOutcome(diligenceOutcome(result, program)); + } else if (job.scene === "dynamic_competition") { + const result = await api.getPublicJobResult(job.id); + const policy = String(job.config.policy ?? "conditional"); + setScene("rescue"); setStrategy(policy); setOutcome(rescueOutcome(result, policy)); + } else if (job.scene === "conversation") { + setConversationResult(await api.getPublicJobResult(job.id)); + setScene("conversation"); + } else { + setStoryResult(await api.getPublicJobResult(job.id)); + setScene("story"); + } + setActiveJobId(null); + } + + if (activeJobId) { + return <> setActiveJobId(null)} onResult={openJobResult} onJobChanged={setActiveJobId} />; + } + + if (!scene) { + return ( + <>
+
+
内InsideGov把政策过程放到桌面上
+ +
+
+
+ AGENT-BASED POLICY SIMULATION +

先把问题
在现实发生前跑一遍。

+

InsideGov 让政府部门、企业和规则在同一个世界里真实互动。你不需要先理解模型,只需要选择一个正在面对的问题。

+ 选择我要解决的问题 ↓ +
+
+ 01

Agent 自主决策不是预写剧情

+ 02

规则确定结算财政、合同和产能可复核

+ 03

结果面向不同人政府、企业和公众各看各的

+
+
+
+
CHOOSE A QUESTION

你今天想先解决哪个问题?

每个入口都是一条完整体验,复杂参数留在过程背后。

+
+ {scenes.map((item) => ( + + ))} +
+
+
+ ONE ENGINE · DIFFERENT EXPERIENCES

底层是一套世界,用户只看与问题有关的那条线。

+
1选择问题→2选择策略→3观察过程→4获得自己的结果
+
+
InsideGov政策实验与组织行为研究工具,不构成现实政策预测。
+
+ ); + } + + if (scene === "conversation") { + return <> setScene(null)} onOpenResearch={onOpenResearch} />; + } + + if (scene === "story") { + return <> setScene(null)} onOpenResearch={onOpenResearch} />; + } + + return ( + <>
+
内InsideGov
+
+ {selectedScene?.eyebrow} · {selectedScene?.index}

{selectedScene?.title}

{selectedScene?.description}

+
  1. 选择策略
  2. 运行过程
  3. 查看结果
+
+ + {!outcome && !busy && ( +
+
STEP 1 / 3

这次想采用哪种处理方式?

我们只改变这一项,其他城市、企业和随机条件保持一致。

+
{strategies[scene].map((item) => )}
+ + {error &&

{error}

} +
+ )} + + {busy &&
内
DEEPSEEK AGENTS ARE RUNNING

各组织正在观察、行动和回应…

所有公众场景固定调用 LLM Agent;世界状态仍由 InsideGov 规则引擎逐步结算。

} + + {outcome && ( + <> +
+
STEP 2 / 3 · GENERATED PROCESS

这次推演实际发生了什么?

+
{outcome.process.map((item, index) =>
{index + 1}
{item.actor}{item.action}

{item.feedback}

)}
+
+
+
STEP 3 / 3

这份结果要给谁看?

+ item.id === strategy)?.name ?? strategy} /> +
+ + )} +
+ ); +} + +type SceneHeading = (typeof scenes)[number] | undefined; + +function ExperienceNav({ onBack, onOpenResearch }: { onBack: () => void; onOpenResearch: () => void }) { + return
内InsideGov
; +} + +function ConversationExperiencePanel({ selectedScene, initialResult, onStartJob, onBack, onOpenResearch }: { selectedScene: SceneHeading; initialResult: ConversationExperience | null; onStartJob: (jobId: string) => void; onBack: () => void; onOpenResearch: () => void }) { + const [mechanisms, setMechanisms] = useState([]); + const [selected, setSelected] = useState("M8"); + const [eventText, setEventText] = useState("企业融资计划反复调整,但尚未公开说明自筹资金缺口。"); + const [result, setResult] = useState(initialResult); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + useEffect(() => { + void api.conversationMechanisms().then(setMechanisms).catch((caught) => setError(String(caught))); + }, []); + + async function run() { + setBusy(true); setError(""); setResult(null); + try { + const job = await api.createPublicJob("conversation", { mechanism_id: selected, event_text: eventText, seed: 42 }); + onStartJob(job.id); + } + catch (caught) { setError(String(caught)); } + finally { setBusy(false); } + } + + return
+ +
{selectedScene?.eyebrow} · {selectedScene?.index}

{selectedScene?.title}

{selectedScene?.description}

  1. 输入条件
  2. 机制运行
  3. 查看证据链
+ {!result && !busy ?
+
STEP 1 / 3

这次项目出现了什么新情况?

系统先把自然语言编译为知情方、可观察信号和透明的世界参数变化,不会直接让模型改数字。

+