diff --git a/scripts/export_agent_daily.py b/scripts/export_agent_daily.py new file mode 100644 index 0000000..aefedef --- /dev/null +++ b/scripts/export_agent_daily.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +""" +Export agent daily messages to Markdown. + +Usage: + python scripts/export_agent_daily.py --agent xingjiang --date 2026-04-07 + python scripts/export_agent_daily.py --agent xingjiang --date 2026-04-07 --output /tmp/report.md +""" + +import argparse +import json +import os +import sys +from datetime import date + +# Add src to path for Django settings +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") + +import django +django.setup() + +from openclaw.models import Session, Message, ToolCall + + +def format_tool_args(args: dict) -> str: + """Format tool arguments as pretty JSON.""" + if not args: + return "" + return json.dumps(args, indent=2, ensure_ascii=False) + + +def export_agent_daily(agent_name: str, target_date: str, output_file=None): + """ + Export all messages for a specific agent on a specific date. + + Sessions may span multiple days, but we focus on messages within the target date. + """ + target_date_obj = date.fromisoformat(target_date) + + # Get sessions for this agent that have messages on the target date + sessions = Session.objects.filter( + agent_name=agent_name + ).prefetch_related("messages", "messages__tool_calls").order_by("start_time") + + daily_sessions = [] + for session in sessions: + # Filter messages to only those on target date + day_messages = session.messages.filter( + timestamp__date=target_date_obj + ).order_by("seq") + + if day_messages.exists(): + daily_sessions.append({ + "session": session, + "messages": list(day_messages), + }) + + if not daily_sessions: + print(f"No messages found for agent {agent_name} on {target_date}", file=sys.stderr) + return + + # Build Markdown output + lines = [] + lines.append(f"# Agent Daily Report: {agent_name} - {target_date}") + lines.append("") + + total_messages = sum(len(s["messages"]) for s in daily_sessions) + lines.append(f"**Total Sessions:** {len(daily_sessions)} ") + lines.append(f"**Total Messages:** {total_messages} ") + lines.append("") + + role_label_map = { + "user": "User", + "assistant": "Assistant", + "tool": "Tool", + "toolResult": "Tool Result", + "system": "System", + } + + for session_data in daily_sessions: + session = session_data["session"] + messages = session_data["messages"] + + start_str = session.start_time.strftime("%Y-%m-%d %H:%M") if session.start_time else "N/A" + end_str = session.end_time.strftime("%H:%M") if session.end_time else " Ongoing" + + lines.append(f"## Session: {session.session_id}") + lines.append(f"**Time:** {start_str} ~ {end_str} ") + lines.append(f"**Model:** {session.model_id or 'N/A'} ") + lines.append(f"**Tokens:** {session.total_tokens:,} | **Cost:** ${session.total_cost:.4f} ") + lines.append(f"**Messages:** {session.message_count} | **Tool Calls:** {session.tool_call_count} ") + lines.append("") + + for msg in messages: + role_disp = role_label_map.get(msg.role, msg.role) + ts_str = msg.timestamp.strftime("%H:%M:%S") + + lines.append(f"### [{msg.seq}] [{role_disp}] [{ts_str}]") + lines.append("") + + # Content + content = msg.content_text.strip() if msg.content_text else "" + if content: + # Truncate very long content + if len(content) > 2000: + content = content[:2000] + "\n\n_[content truncated]_" + lines.append(f"**Content:**\n{content}") + else: + lines.append("**Content:** _(empty)_") + lines.append("") + + # Tool call info + if msg.tool_name: + lines.append(f"**Tool:** `{msg.tool_name}` | **Exit:** {msg.exit_code} | **Duration:** {msg.duration_ms}ms") + lines.append("") + + # Get tool calls for this message + tool_calls = msg.tool_calls.all() + if tool_calls.exists(): + for tc in tool_calls: + args_str = format_tool_args(tc.arguments) + if args_str: + lines.append(f"**Arguments:**\n```json\n{args_str}\n```") + lines.append("") + if tc.result_text: + result = tc.result_text.strip() + if len(result) > 1000: + result = result[:1000] + "\n\n_[result truncated]_" + lines.append(f"**Result:**\n{result}") + lines.append("") + + # Token info + if msg.tokens_total > 0: + lines.append(f"**Tokens:** {msg.tokens_total:,} (in: {msg.tokens_input}, out: {msg.tokens_output})") + + if msg.is_error: + lines.append("⚠️ **Error**") + + lines.append("") + lines.append("---") + lines.append("") + + md_content = "\n".join(lines) + + if output_file: + with open(output_file, "w", encoding="utf-8") as f: + f.write(md_content) + print(f"Report written to: {output_file}", file=sys.stderr) + else: + print(md_content) + + +def main(): + parser = argparse.ArgumentParser( + description="Export agent daily messages to Markdown" + ) + parser.add_argument("--agent", required=True, help="Agent name (e.g. xingjiang)") + parser.add_argument("--date", required=True, help="Target date (YYYY-MM-DD)") + parser.add_argument("--output", help="Output file path (default: stdout)") + + args = parser.parse_args() + + # Validate date format + try: + date.fromisoformat(args.date) + except ValueError: + print(f"Error: Invalid date format {args.date}. Use YYYY-MM-DD.", file=sys.stderr) + sys.exit(1) + + export_agent_daily(args.agent, args.date, args.output) + + +if __name__ == "__main__": + main() diff --git a/src/openclaw/admin.py b/src/openclaw/admin.py index 5a570c8..990533b 100644 --- a/src/openclaw/admin.py +++ b/src/openclaw/admin.py @@ -145,11 +145,15 @@ class SessionAdmin(admin.ModelAdmin): @admin.register(Message) class MessageAdmin(admin.ModelAdmin): - list_display = ("message_id", "session", "role", "timestamp", "tokens_total") + list_display = ("message_id", "get_agent_name", "session", "role", "timestamp", "tokens_total") list_filter = ("role", "model", "timestamp") search_fields = ("content_text",) ordering = ("-timestamp",) + @admin.display(description="Agent") + def get_agent_name(self, obj): + return obj.session.agent_name + @admin.register(ToolCall) class ToolCallAdmin(admin.ModelAdmin):