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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.finals.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Host-side paths consumed by docker-compose.gemma4-demo.yml.
# Copy to .env.finals and pass it with: docker compose --env-file .env.finals ...
PAPER9_HOST_REPO=/absolute/path/to/paper9-mnr-offline-package
PAPER9_BISHAN_RUNS_HOST=/absolute/path/to/bishan-runs
PAPER9_DONGXING_RUNS_HOST=/absolute/path/to/dongxing-runs

# Ollama must expose the exact Gemma4:26b tag.
OLLAMA_API_BASE=http://host.docker.internal:11434
EMBEDDING_MODEL=nomic-embed-text-v2-moe
198 changes: 111 additions & 87 deletions README.md

Large diffs are not rendered by default.

25 changes: 2 additions & 23 deletions data_agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
from .toolsets.world_model_v2_tools import WorldModelV2Toolset
from .toolsets.world_model_v21_tools import WorldModelV21Toolset
from .toolsets.territory_world_model_tools import TerritoryWorldModelToolset
from .paper9_agent_prompt import PAPER9_AGENT_INSTRUCTION

# ArcPy conditional function lists (for governance agents needing specific subsets)
from .toolsets.geo_processing_tools import (
Expand Down Expand Up @@ -1076,29 +1077,7 @@ def _build_mention_nl2sql_agent():
def _build_mention_world_model_v21_agent():
return LlmAgent(
name="MentionWorldModelV21",
instruction=(
"你是世界模型 v2.1 演示代理,只使用可用的 world_model_v21_status、"
"world_model_v21_prepare、world_model_v21_sample、world_model_v21_train、"
"world_model_v21_plan 和 world_model_v21_pipeline 工具。必须先调用 "
"world_model_v21_status 检查 Paper9 仓库、默认 prepared_dir 和 ensemble_dir。"
"如果用户要求完整 A->B->C->D 链路,调用 world_model_v21_pipeline;"
"如果用户只要求规划或演示,默认调用 world_model_v21_pipeline 并设置 reuse_existing=true,"
"以展示 Tool 1/2/3 复用和 Tool 4 实际规划;只有用户明确要求“只运行 Tool 4”"
"或“只调用 plan”时,才调用 world_model_v21_plan。"
"必须识别数据集名称:用户提到 dongxing 或东兴时,调用工具必须设置 dataset='dongxing';"
"用户提到 Bishan 或璧山时,调用工具必须设置 dataset='bishan'。"
"只有用户没有提到数据集时,才使用默认 dataset='bishan'。"
"用户未提供 prepared_dir 或 ensemble_dir 时,对应参数保持空字符串,让工具根据 dataset 选择演示路径。"
"默认使用 env_kind=county, "
"horizon=1, top_k=1, n_episodes=1, continuation=greedy, scoring=reward,"
"除非用户明确指定其他参数。最终用中文简要总结 status/version/mode/env_kind/"
"steps_run/n_blocks/n_selected/total_reward/artifacts,并明确列出工具调用轨迹:"
"优先展示 world_model_v21_status -> world_model_v21_pipeline。不要输出英文 Plan/Step、"
"思考过程、参数复述或重试纠错过程;如果工具重试后成功,只报告最终成功结果。"
"说明 horizon 是 MPC 前瞻步长,steps_run 是环境实际执行步数,两者不是同一个指标。"
"如果结果包含县域优化地图图层,明确说明右侧地图按 CHG_FLAG 展示优化变化:"
"灰色为保持不变,红色为耕地 -> 林地,绿色为林地 -> 耕地。"
),
instruction=PAPER9_AGENT_INSTRUCTION,
description="世界模型 v2.1 状态检查与 MPC 规划。",
model=get_model_for_tier("standard"),
output_key="analysis_result",
Expand Down
193 changes: 141 additions & 52 deletions data_agent/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1730,6 +1730,10 @@ async def _execute_pipeline(
_final_chart_updates = [] # accumulated chart configs
_world_model_v21_result = None
_world_model_v21_args = None
_world_model_v21_status = None
_paper9_audit_result = None
_paper9_commit_result = None
_world_model_v21_trace = []
_drl_optimization_result = None
_drl_optimization_args = None
_drl_comparison_map_seen = False
Expand Down Expand Up @@ -1757,7 +1761,16 @@ async def _execute_pipeline(
await progress_msg.send()

try:
run_config = RunConfig(max_llm_calls=50) if (DYNAMIC_PLANNER and pipeline_type == "planner") else None
_selected_agent_name = getattr(selected_agent, "name", "") or ""
if DYNAMIC_PLANNER and pipeline_type == "planner":
run_config = RunConfig(max_llm_calls=50)
elif (
pipeline_type == "sub_agent_direct"
and _selected_agent_name == "MentionWorldModelV21"
):
run_config = RunConfig(max_llm_calls=12)
else:
run_config = None
events = runner.run_async(user_id=user_id, session_id=session_id, new_message=content, run_config=run_config)

# Token accumulation counters
Expand Down Expand Up @@ -1935,6 +1948,29 @@ async def _execute_pipeline(
except Exception:
pass
try:
_paper9_trace_tools = {
"world_model_v21_status",
"paper9_inspect_resources",
"paper9_recall_verified_episodes",
"world_model_v21_prepare",
"world_model_v21_sample",
"world_model_v21_train",
"world_model_v21_plan",
"world_model_v21_pipeline",
"paper9_audit_run",
"paper9_commit_verified_episode",
}
if current_tool_name in _paper9_trace_tools:
_trace_duration = None
if _pending_tool_call:
_trace_duration = (
time.time() - _pending_tool_call["start_time"]
)
_world_model_v21_trace.append({
"tool_name": current_tool_name,
"duration_s": _trace_duration,
})

if current_tool_name in {"world_model_v21_plan", "world_model_v21_pipeline"}:
from data_agent.world_model_v21_presentation import parse_world_model_v21_tool_response

Expand All @@ -1946,6 +1982,20 @@ async def _execute_pipeline(
if _pending_tool_call else {}
)
logger.info("[WorldModelV21Presentation] Captured planning result for deterministic summary")
elif current_tool_name in {
"world_model_v21_status",
"paper9_audit_run",
"paper9_commit_verified_episode",
}:
from data_agent.world_model_v21_presentation import parse_structured_tool_response

_parsed_paper9 = parse_structured_tool_response(_resp_val)
if current_tool_name == "world_model_v21_status":
_world_model_v21_status = _parsed_paper9
elif current_tool_name == "paper9_audit_run":
_paper9_audit_result = _parsed_paper9
elif current_tool_name == "paper9_commit_verified_episode":
_paper9_commit_result = _parsed_paper9
except Exception as _wm_present_capture_err:
logger.debug("WorldModelV21 presentation capture skipped: %s", _wm_present_capture_err)
try:
Expand Down Expand Up @@ -2364,11 +2414,11 @@ async def _execute_pipeline(
)
if _is_nl2sql_direct and full_response_text:
from data_agent.nl2sql_presentation import (
build_bridge_building_map_update,
build_nl2sql_map_update,
describe_map_update,
format_nl2sql_result_for_chat,
)
_nl2sql_map_update = build_bridge_building_map_update(
_nl2sql_map_update = build_nl2sql_map_update(
full_response_text,
question=full_prompt,
)
Expand Down Expand Up @@ -2402,10 +2452,19 @@ async def _execute_pipeline(
full_response_text = format_world_model_v21_result_for_chat(
_world_model_v21_result,
tool_args=_world_model_v21_args or {},
status_result=_world_model_v21_status,
audit_result=_paper9_audit_result,
commit_result=_paper9_commit_result,
tool_trace=_world_model_v21_trace,
total_duration_s=total_duration,
)
progress_msg.content = format_world_model_v21_progress_for_chat(
_world_model_v21_result,
pipeline_label=pipeline_name,
audit_result=_paper9_audit_result,
commit_result=_paper9_commit_result,
tool_trace=_world_model_v21_trace,
total_duration_s=total_duration,
)
await progress_msg.update()
except Exception as _wm_present_err:
Expand Down Expand Up @@ -2519,11 +2578,24 @@ async def _execute_pipeline(
output_path = tool_step.get("output_path")
if output_path and os.path.exists(output_path) and output_path not in generated_files:
generated_files.append(output_path)
cl.user_session.set("last_context", {
last_context_payload = {
"pipeline": pipeline_type,
"files": generated_files,
"summary": report_text[:800] if report_text else "",
})
}
if _world_model_v21_result:
# Preserve the structured evidence needed to rebuild a visual,
# run-specific report. Public chat text intentionally omits paths.
last_context_payload["world_model_v21_report"] = {
"result": _world_model_v21_result,
"tool_args": _world_model_v21_args or {},
"status_result": _world_model_v21_status or {},
"audit_result": _paper9_audit_result or {},
"commit_result": _paper9_commit_result or {},
"tool_trace": _world_model_v21_trace,
"total_duration_s": total_duration,
}
cl.user_session.set("last_context", last_context_payload)
cl.user_session.set("tool_execution_log", tool_execution_log)
cl.user_session.set("last_intent", intent)

Expand Down Expand Up @@ -4600,61 +4672,78 @@ async def on_export_report(action: cl.Action):
await msg.send()
try:
user_dir = get_user_upload_dir()
last_ctx = cl.user_session.get("last_context", {}) or {}
world_model_report = last_ctx.get("world_model_v21_report")

# Enrich report text with recent PNG visualizations for image embedding
enriched_text = text
try:
import glob
import time as _time

# Prefer PNGs that were explicitly generated in this session's context
last_ctx = cl.user_session.get("last_context", {})
session_files = last_ctx.get("files", [])
recent_pngs = [f for f in session_files if f.lower().endswith(".png") and os.path.exists(f)]

# Fallback to scanning the directory for very recent PNGs (last 5 mins)
if not recent_pngs:
recent_pngs = sorted(
glob.glob(os.path.join(user_dir, "*.png")),
key=os.path.getmtime, reverse=True
if isinstance(world_model_report, dict) and world_model_report.get("result"):
from data_agent.world_model_v21_report import (
generate_world_model_v21_pdf_report,
generate_world_model_v21_word_report,
)

if fmt == "pdf":
output_path = os.path.join(user_dir, "County_Farmland_Planning_Report.pdf")
result_path = generate_world_model_v21_pdf_report(
world_model_report, output_path, author=author
)
cutoff = _time.time() - 300
recent_pngs = [p for p in recent_pngs if os.path.getmtime(p) > cutoff]
else:
output_path = os.path.join(user_dir, "County_Farmland_Planning_Report.docx")
result_path = generate_world_model_v21_word_report(
world_model_report, output_path, author=author
)
else:
# Enrich general reports with session PNG visualizations when available.
enriched_text = text
try:
import glob
import time as _time

session_files = last_ctx.get("files", [])
recent_pngs = [
f for f in session_files
if f.lower().endswith(".png") and os.path.exists(f)
]
if not recent_pngs:
recent_pngs = sorted(
glob.glob(os.path.join(user_dir, "*.png")),
key=os.path.getmtime,
reverse=True,
)
cutoff = _time.time() - 300
recent_pngs = [
path for path in recent_pngs
if os.path.getmtime(path) > cutoff
]

if recent_pngs:
# Deduplicate and normalize paths
unique_pngs = []
for p in recent_pngs:
norm_p = os.path.abspath(p)
if norm_p not in unique_pngs:
unique_pngs.append(norm_p)

# Only append if not already prominently featured in the text
for path in recent_pngs:
normalized = os.path.abspath(path)
if normalized not in unique_pngs:
unique_pngs.append(normalized)
images_to_add = []
for p in unique_pngs:
p_unix = p.replace("\\", "/")
p_win = p.replace("/", "\\")
if p_unix not in text and p_win not in text:
images_to_add.append(p)

for path in unique_pngs:
if path.replace("\\", "/") not in text and path.replace("/", "\\") not in text:
images_to_add.append(path)
if images_to_add:
enriched_text += "\n\n## 分析可视化成果\n\n"
for png_path in images_to_add[:4]: # max 4 images
for png_path in images_to_add[:4]:
enriched_text += f"{png_path}\n\n"
except Exception as _enrich_err:
logger.warning("Report enrichment failed: %s", _enrich_err)
if fmt == "pdf":
from data_agent.report_generator import generate_pdf_report
output_path = os.path.join(user_dir, "Analysis_Report.pdf")
result_path = generate_pdf_report(
enriched_text, output_path, author=author, pipeline_type=pipeline_type
)
else:
output_path = os.path.join(user_dir, "Analysis_Report.docx")
generate_word_report(
enriched_text, output_path, author=author, pipeline_type=pipeline_type
)
result_path = output_path
except Exception as _enrich_err:
logger.warning("Report enrichment failed: %s", _enrich_err)

if fmt == "pdf":
from data_agent.report_generator import generate_pdf_report

output_path = os.path.join(user_dir, "Analysis_Report.pdf")
result_path = generate_pdf_report(
enriched_text, output_path, author=author, pipeline_type=pipeline_type
)
else:
output_path = os.path.join(user_dir, "Analysis_Report.docx")
generate_word_report(
enriched_text, output_path, author=author, pipeline_type=pipeline_type
)
result_path = output_path

sync_to_obs(result_path)
filename = os.path.basename(result_path)
Expand Down
Loading
Loading