#!/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(" 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)