cai/tools/case_study_generator.py

446 lines
17 KiB
Python

#!/usr/bin/env python3
"""
Example:
CAI_MODEL="claude-sonnet-4-20250514" CAI_STREAM=True python3 case_study_generator.py --jsonl_file logs/cai_b97af8fc-3d51-45d3-8393-6c3341d33807_20250602_201144_luijait_darwin_24.5.0_81_38_189_27.jsonl --output_php_file alias_web/case_study_test.php
CAI Case Study Generator - Generate PHP case studies from JSONL files.
This script loads context from JSONL files using the same mechanism as CAI's /load command,
runs the UseCase agent with streaming output, and generates PHP case studies.
Usage:
python case_study_generator.py --jsonl_file logs/session.jsonl --output_php_file output.php
python case_study_generator.py --jsonl_file logs/last --output_php_file case_studies/latest.php
"""
import os
from dotenv import load_dotenv
# Load .env from current directory only, not from parent directories
dotenv_path = os.path.join(os.getcwd(), '.env')
load_dotenv(dotenv_path=dotenv_path, verbose=False)
# Set default for OPENAI_API_KEY if not already set
if "OPENAI_API_KEY" not in os.environ:
os.environ["OPENAI_API_KEY"] = ""
import sys
import asyncio
import argparse
from pathlib import Path
import json
import re
from typing import List, Dict, Any, Optional
# Import CAI SDK components
from cai.sdk.agents import Runner
from cai.sdk.agents.models.openai_chatcompletions import message_history, add_to_message_history
from cai.sdk.agents.run_to_jsonl import load_history_from_jsonl
from cai.sdk.agents.stream_events import RunItemStreamEvent
from cai.sdk.agents.items import ToolCallOutputItem
# Import UseCase agent
from src.cai.agents.usecase import use_case_agent
# Rich console for better output
from rich.console import Console
from rich.panel import Panel
from rich.live import Live
from rich.text import Text
from rich.progress import Progress, SpinnerColumn, TextColumn
console = Console()
def extract_php_code(text: str) -> Optional[str]:
"""Extract PHP code from markdown code blocks."""
if not text:
return None
# Try to extract PHP code between ```php and ```
php_matches = re.findall(r"```php\n(.*?)```", text, re.DOTALL)
if php_matches:
return php_matches[0].strip()
# If no code blocks, check if the entire text looks like PHP
if text.strip().startswith("<?php") or text.strip().startswith("<!doctype"):
return text.strip()
return None
async def generate_case_study(jsonl_file: str, output_php_file: str) -> Optional[str]:
"""
Generate a PHP case study from a JSONL file using streaming output.
Args:
jsonl_file: Path to the JSONL file to load context from
output_php_file: Path to save the PHP output to
Returns:
Path to the saved PHP file or None if failed
"""
# Clear any existing messages in message_history to start fresh
message_history.clear()
# Load context from JSONL file (simulating /load command)
try:
console.print(f"[yellow]Loading JSONL file: {jsonl_file}[/yellow]")
messages = load_history_from_jsonl(jsonl_file)
if not messages:
console.print("[red]Error: No messages found in JSONL file[/red]")
return None
console.print(f"[green]✓ Loaded {len(messages)} messages from JSONL[/green]")
# Add messages to message_history (exactly like /load command does)
for message in messages:
message_history.append(message)
# Display loaded context summary
user_messages = sum(1 for msg in messages if msg.get("role") == "user")
assistant_messages = sum(1 for msg in messages if msg.get("role") == "assistant")
tool_messages = sum(1 for msg in messages if msg.get("role") == "tool")
console.print(
Panel(
f"Context loaded:\n"
f"• User messages: {user_messages}\n"
f"• Assistant messages: {assistant_messages}\n"
f"• Tool messages: {tool_messages}",
title="[bold]JSONL Context Summary[/bold]",
border_style="blue",
)
)
except Exception as e:
console.print(f"[red]Error loading JSONL file: {str(e)}[/red]")
return None
# Analyze the loaded context to provide better guidance
context_summary = []
if messages:
# Find the main topic/challenge from user messages
for msg in messages:
if msg.get("role") == "user" and msg.get("content"):
content = msg.get("content", "")[:300] # First 300 chars
if content and len(content) > 20: # Skip very short messages
context_summary.append(content.strip())
if len(context_summary) >= 5: # Get first few meaningful messages
break
# Generate case study prompt with context
prompt = "Generate the PHP code for a cybersecurity case study based on the template. "
prompt += "Analyze the conversation context that has been loaded and create a comprehensive case study. "
prompt += "Fill in all TEMPLATE-TODO sections with relevant information from the session. "
prompt += "Explain step by step the problem and the solution in this escenario"
prompt += "The output should be complete PHP code ready to save to a file."
# Add a summary of the JSONL conversation to the prompt
if messages:
prompt += "\n\n## Conversation Context from JSONL:\n"
# Get key information from the conversation
user_msgs = [msg for msg in messages if msg.get("role") == "user"]
assistant_msgs = [msg for msg in messages if msg.get("role") == "assistant"]
tool_msgs = [msg for msg in messages if msg.get("role") == "tool"]
# Add user messages
if user_msgs:
prompt += "\n### User Messages:\n"
for i, msg in enumerate(user_msgs[:5], 1):
content = msg.get("content", "")[:500]
if content:
prompt += f"{i}. {content}\n"
# Add key assistant responses
if assistant_msgs:
prompt += "\n### Key Assistant Responses:\n"
for i, msg in enumerate(assistant_msgs[:3], 1):
content = msg.get("content", "")[:500]
if content and "I'll help" not in content: # Skip generic responses
prompt += f"{i}. {content}\n"
# Add tool outputs that might contain important data
if tool_msgs:
prompt += "\n### Tool Outputs (key findings):\n"
important_tools = []
for msg in tool_msgs:
content = msg.get("content", "")
# Look for important patterns in tool output
if any(
keyword in content.lower()
for keyword in [
"map",
"credential",
"password",
"auth",
"endpoint",
"192.168",
"http",
]
):
important_tools.append(content[:500])
for i, content in enumerate(important_tools[:5], 1):
prompt += f"{i}. {content}\n"
console.print(f"\n[cyan]Generating case study with UseCase agent...[/cyan]")
# Configure streaming mode based on environment variable
stream_mode = os.getenv("CAI_STREAM", "true").lower() != "false"
try:
if stream_mode:
# Streaming mode - similar to CLI implementation
console.print("[dim]Using streaming mode...[/dim]")
# Track if we've seen any output
has_output = False
accumulated_text = []
php_code = None
# Run the streaming process like CLI does
async def process_streamed_response():
try:
result_stream = Runner.run_streamed(use_case_agent, prompt)
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
transient=True,
) as progress:
task = progress.add_task(
"[cyan]Processing with UseCase agent...", total=None
)
# Consume events so the async generator is executed
async for event in result_stream.stream_events():
if isinstance(event, RunItemStreamEvent):
# Handle tool outputs
if event.name == "tool_output" and isinstance(
event.item, ToolCallOutputItem
):
progress.update(
task,
description=f"[cyan]Tool: {event.item.raw_item.get('name', 'unknown')}...",
)
# Add tool message to history (like CLI does)
tool_msg = {
"role": "tool",
"tool_call_id": event.item.raw_item["call_id"],
"content": event.item.output,
}
add_to_message_history(tool_msg)
progress.update(task, description="[green]Finalizing output...")
# The result is available after streaming completes
# But we need to extract the output from message_history
# since streaming doesn't provide direct access to final output
# Get the last assistant message from message_history
for msg in reversed(message_history):
if msg.get("role") == "assistant" and msg.get("content"):
return msg.get("content")
return None
except Exception as e:
console.print(f"[red]Error in streaming: {str(e)}[/red]")
import traceback
console.print(f"[red]{traceback.format_exc()}[/red]")
return None
# Run the streaming process
final_output = await process_streamed_response()
if final_output:
php_code = extract_php_code(final_output)
if not php_code:
php_code = final_output
if php_code:
console.print(f"[green]✓ Generated {len(php_code)} characters of output[/green]")
else:
console.print("[red]Error: No output from UseCase agent[/red]")
return None
else:
# Non-streaming mode (simpler, like in examples)
console.print("[dim]Using non-streaming mode...[/dim]")
# Show progress
with console.status("[bold green]Generating case study...") as status:
# Instead of passing the conversation history directly,
# just use the prompt with all the context embedded in it
# This avoids issues with incomplete tool call/response pairs
# Run with just the prompt
result = await Runner.run(use_case_agent, prompt)
# Extract PHP code from result
if hasattr(result, "final_output") and result.final_output:
output_text = result.final_output
# Process the output to handle tool outputs
for item in result.new_items:
if isinstance(item, ToolCallOutputItem):
# Add tool messages to history
tool_msg = {
"role": "tool",
"tool_call_id": item.raw_item["call_id"],
"content": item.output,
}
add_to_message_history(tool_msg)
php_code = extract_php_code(output_text)
if not php_code:
# If extraction failed, use the raw output
php_code = output_text
console.print(f"[green]✓ Generated {len(php_code)} characters of output[/green]")
else:
console.print("[red]Error: No output from UseCase agent[/red]")
return None
except Exception as e:
console.print(f"[red]Error generating case study: {str(e)}[/red]")
import traceback
console.print(f"[red]{traceback.format_exc()}[/red]")
return None
# Validate PHP code
if not php_code or len(php_code) < 100:
console.print("[red]Error: Generated output is too short or invalid[/red]")
return None
# Save PHP code to file
try:
output_path = Path(output_php_file)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
f.write(php_code)
console.print(f"\n[green]✓ PHP case study saved to: {output_php_file}[/green]")
# Display file size and preview
file_size = output_path.stat().st_size
console.print(f"[dim]File size: {file_size:,} bytes[/dim]")
# Show first few lines as preview
lines = php_code.split("\n")[:15]
preview = "\n".join(lines)
if len(php_code.split("\n")) > 15:
preview += "\n..."
console.print(Panel(preview, title="[bold]PHP File Preview[/bold]", border_style="blue"))
return str(output_path)
except Exception as e:
console.print(f"[red]Error saving PHP file: {str(e)}[/red]")
return None
def parse_args():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description="Generate PHP case studies from JSONL files using CAI UseCase agent.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Generate case study from a specific JSONL file
python case_study_generator.py --jsonl_file logs/session_20240102_123456.jsonl --output_php_file case_studies/ctf_writeup.php
# Use the last session log (default behavior like /load command)
python case_study_generator.py --jsonl_file logs/last --output_php_file case_studies/latest.php
# Generate with custom output directory
python case_study_generator.py --jsonl_file logs/last --output_php_file ~/Documents/case_studies/analysis.php
# Override the model
python case_study_generator.py --jsonl_file logs/last --output_php_file output.php --model gpt-4o
# Disable streaming
CAI_STREAM=false python case_study_generator.py --jsonl_file logs/last --output_php_file output.php
""",
)
parser.add_argument(
"--jsonl_file",
type=str,
default="logs/last",
help="Path to the JSONL file containing conversation context (default: logs/last)",
)
parser.add_argument(
"--output_php_file",
type=str,
required=True,
help="Path where the generated PHP file will be saved",
)
parser.add_argument(
"--model",
type=str,
default=None,
help="Override the model to use (e.g., claude-sonnet-4-20250514, gpt-4o)",
)
return parser.parse_args()
async def main():
"""Main entry point for the script."""
args = parse_args()
# Display banner
console.print(
Panel(
"[bold cyan]CAI Case Study Generator[/bold cyan]\n"
"Generate professional cybersecurity case studies from JSONL session logs\n\n"
"[dim]This tool uses the CAI UseCase agent to analyze session context and generate\n"
"comprehensive PHP case studies based on the conversation history.[/dim]",
border_style="cyan",
)
)
# Override model if specified
if args.model:
os.environ["CAI_MODEL"] = args.model
console.print(f"[yellow]Using model override: {args.model}[/yellow]")
current_model = os.getenv("CAI_MODEL", "alias1")
console.print(f"[yellow]Model: {current_model}[/yellow]")
# Check if JSONL file exists
jsonl_path = Path(args.jsonl_file)
if not jsonl_path.exists() and args.jsonl_file != "logs/last":
console.print(f"[red]Error: JSONL file not found: {args.jsonl_file}[/red]")
return 1
# Generate the case study
result = await generate_case_study(args.jsonl_file, args.output_php_file)
if result:
console.print("\n[bold green]✨ Case study generation completed successfully![/bold green]")
console.print(f"[dim]You can now open {result} in your browser or editor[/dim]")
return 0
else:
console.print("\n[bold red]❌ Case study generation failed[/bold red]")
console.print("[dim]Please check the error messages above and ensure:[/dim]")
console.print("[dim]1. The JSONL file contains valid session data[/dim]")
console.print("[dim]2. The UseCase agent has access to the template file[/dim]")
console.print("[dim]3. Your API keys are properly configured[/dim]")
return 1
if __name__ == "__main__":
# Run the async main function
exit_code = asyncio.run(main())
sys.exit(exit_code)