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>
249 lines
8.1 KiB
Python
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} • <a href='{APP_URL}'>{APP_URL}</a></p>",
|
|
" </div>",
|
|
"</body>",
|
|
"</html>",
|
|
])
|
|
|
|
return "\n".join(html_parts)
|