Files
oai/oai/utils/export.py
Rune Olsen b0cf88704e 2.1 (#2)
Final release of version 2.1.

Headlights:

### Core Features
- 🤖 Interactive chat with 300+ AI models via OpenRouter
- 🔍 Model selection with search and filtering
- 💾 Conversation save/load/export (Markdown, JSON, HTML)
- 📎 File attachments (images, PDFs, code files)
- 💰 Real-time cost tracking and credit monitoring
- 🎨 Rich terminal UI with syntax highlighting
- 📝 Persistent command history with search (Ctrl+R)
- 🌐 Online mode (web search capabilities)
- 🧠 Conversation memory toggle

### MCP Integration
- 🔧 **File Mode**: AI can read, search, and list local files
  - Automatic .gitignore filtering
  - Virtual environment exclusion
  - Large file handling (auto-truncates >50KB)

- ✍️ **Write Mode**: AI can modify files with permission
  - Create, edit, delete files
  - Move, copy, organize files
  - Always requires explicit opt-in

- 🗄️ **Database Mode**: AI can query SQLite databases
  - Read-only access (safe)
  - Schema inspection
  - Full SQL query support

Reviewed-on: #2
Co-authored-by: Rune Olsen <rune@rune.pm>
Co-committed-by: Rune Olsen <rune@rune.pm>
2026-02-03 09:02:44 +01:00

249 lines
8.1 KiB
Python

"""
Export utilities for oAI.
This module provides functions for exporting conversation history
in various formats including Markdown, JSON, and HTML.
"""
import json
import datetime
from typing import List, Dict
from html import escape as html_escape
from oai.constants import APP_VERSION, APP_URL
def export_as_markdown(
session_history: List[Dict[str, str]],
session_system_prompt: str = ""
) -> str:
"""
Export conversation history as Markdown.
Args:
session_history: List of message dictionaries with 'prompt' and 'response'
session_system_prompt: Optional system prompt to include
Returns:
Markdown formatted string
"""
lines = ["# Conversation Export", ""]
if session_system_prompt:
lines.extend([f"**System Prompt:** {session_system_prompt}", ""])
lines.append(f"**Export Date:** {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
lines.append(f"**Messages:** {len(session_history)}")
lines.append("")
lines.append("---")
lines.append("")
for i, entry in enumerate(session_history, 1):
lines.append(f"## Message {i}")
lines.append("")
lines.append("**User:**")
lines.append("")
lines.append(entry.get("prompt", ""))
lines.append("")
lines.append("**Assistant:**")
lines.append("")
lines.append(entry.get("response", ""))
lines.append("")
lines.append("---")
lines.append("")
lines.append(f"*Exported from oAI v{APP_VERSION} - {APP_URL}*")
return "\n".join(lines)
def export_as_json(
session_history: List[Dict[str, str]],
session_system_prompt: str = ""
) -> str:
"""
Export conversation history as JSON.
Args:
session_history: List of message dictionaries
session_system_prompt: Optional system prompt to include
Returns:
JSON formatted string
"""
export_data = {
"export_date": datetime.datetime.now().isoformat(),
"app_version": APP_VERSION,
"system_prompt": session_system_prompt,
"message_count": len(session_history),
"messages": [
{
"index": i + 1,
"prompt": entry.get("prompt", ""),
"response": entry.get("response", ""),
"prompt_tokens": entry.get("prompt_tokens", 0),
"completion_tokens": entry.get("completion_tokens", 0),
"cost": entry.get("msg_cost", 0.0),
}
for i, entry in enumerate(session_history)
],
"totals": {
"prompt_tokens": sum(e.get("prompt_tokens", 0) for e in session_history),
"completion_tokens": sum(e.get("completion_tokens", 0) for e in session_history),
"total_cost": sum(e.get("msg_cost", 0.0) for e in session_history),
}
}
return json.dumps(export_data, indent=2, ensure_ascii=False)
def export_as_html(
session_history: List[Dict[str, str]],
session_system_prompt: str = ""
) -> str:
"""
Export conversation history as styled HTML.
Args:
session_history: List of message dictionaries
session_system_prompt: Optional system prompt to include
Returns:
HTML formatted string with embedded CSS
"""
html_parts = [
"<!DOCTYPE html>",
"<html>",
"<head>",
" <meta charset='UTF-8'>",
" <meta name='viewport' content='width=device-width, initial-scale=1.0'>",
" <title>Conversation Export - oAI</title>",
" <style>",
" * { box-sizing: border-box; }",
" body {",
" font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;",
" max-width: 900px;",
" margin: 40px auto;",
" padding: 20px;",
" background: #f5f5f5;",
" color: #333;",
" }",
" .header {",
" background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);",
" color: white;",
" padding: 30px;",
" border-radius: 10px;",
" margin-bottom: 30px;",
" box-shadow: 0 4px 6px rgba(0,0,0,0.1);",
" }",
" .header h1 {",
" margin: 0 0 10px 0;",
" font-size: 2em;",
" }",
" .export-info {",
" opacity: 0.9;",
" font-size: 0.95em;",
" margin: 5px 0;",
" }",
" .system-prompt {",
" background: #fff3cd;",
" padding: 20px;",
" border-radius: 8px;",
" margin-bottom: 25px;",
" border-left: 5px solid #ffc107;",
" box-shadow: 0 2px 4px rgba(0,0,0,0.05);",
" }",
" .system-prompt strong {",
" color: #856404;",
" display: block;",
" margin-bottom: 10px;",
" font-size: 1.1em;",
" }",
" .message-container { margin-bottom: 20px; }",
" .message {",
" background: white;",
" padding: 20px;",
" border-radius: 8px;",
" box-shadow: 0 2px 4px rgba(0,0,0,0.08);",
" margin-bottom: 12px;",
" }",
" .user-message { border-left: 5px solid #10b981; }",
" .assistant-message { border-left: 5px solid #3b82f6; }",
" .role {",
" font-weight: bold;",
" margin-bottom: 12px;",
" font-size: 1.05em;",
" text-transform: uppercase;",
" letter-spacing: 0.5px;",
" }",
" .user-role { color: #10b981; }",
" .assistant-role { color: #3b82f6; }",
" .content {",
" line-height: 1.8;",
" white-space: pre-wrap;",
" color: #333;",
" }",
" .message-number {",
" color: #6b7280;",
" font-size: 0.85em;",
" margin-bottom: 15px;",
" font-weight: 600;",
" }",
" .footer {",
" text-align: center;",
" margin-top: 40px;",
" padding: 20px;",
" color: #6b7280;",
" font-size: 0.9em;",
" }",
" .footer a { color: #667eea; text-decoration: none; }",
" .footer a:hover { text-decoration: underline; }",
" @media print {",
" body { background: white; }",
" .message { break-inside: avoid; }",
" }",
" </style>",
"</head>",
"<body>",
" <div class='header'>",
" <h1>Conversation Export</h1>",
f" <div class='export-info'>Exported: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</div>",
f" <div class='export-info'>Total Messages: {len(session_history)}</div>",
" </div>",
]
if session_system_prompt:
html_parts.extend([
" <div class='system-prompt'>",
" <strong>System Prompt</strong>",
f" <div>{html_escape(session_system_prompt)}</div>",
" </div>",
])
for i, entry in enumerate(session_history, 1):
prompt = html_escape(entry.get("prompt", ""))
response = html_escape(entry.get("response", ""))
html_parts.extend([
" <div class='message-container'>",
f" <div class='message-number'>Message {i} of {len(session_history)}</div>",
" <div class='message user-message'>",
" <div class='role user-role'>User</div>",
f" <div class='content'>{prompt}</div>",
" </div>",
" <div class='message assistant-message'>",
" <div class='role assistant-role'>Assistant</div>",
f" <div class='content'>{response}</div>",
" </div>",
" </div>",
])
html_parts.extend([
" <div class='footer'>",
f" <p>Generated by oAI v{APP_VERSION} &bull; <a href='{APP_URL}'>{APP_URL}</a></p>",
" </div>",
"</body>",
"</html>",
])
return "\n".join(html_parts)