Spaces:
Sleeping
Sleeping
| """ | |
| MozeAI Document Studio - Complete Application | |
| Intelligent Document Workspace with AI Memory, Streaming, and Multi-Turn Context | |
| """ | |
| import streamlit as st | |
| from groq import Groq | |
| import requests | |
| import re | |
| import numpy as np | |
| from bs4 import BeautifulSoup | |
| import PyPDF2 | |
| import docx | |
| from io import StringIO, BytesIO | |
| import csv | |
| import json | |
| from datetime import datetime | |
| import pytz | |
| import time | |
| import hashlib | |
| from collections import defaultdict | |
| import os | |
| from difflib import unified_diff | |
| import uuid | |
| from typing import Dict, List, Optional, Callable, Any, Tuple | |
| from dataclasses import dataclass, field, asdict | |
| # ============================================================================ | |
| # DOCUMENT GENERATION LIBRARIES | |
| # ============================================================================ | |
| from pptx import Presentation | |
| from pptx.util import Inches, Pt | |
| from pptx.enum.text import PP_ALIGN | |
| from pptx.dml.color import RGBColor | |
| from docx import Document as WordDocument | |
| from docx.shared import Inches as DocInches, Pt as DocPt | |
| from docx.enum.text import WD_ALIGN_PARAGRAPH | |
| # ============================================================================ | |
| # DATA CLASSES FOR INTELLIGENCE CORE | |
| # ============================================================================ | |
| class ParsedInstruction: | |
| """Structured representation of user instruction""" | |
| intent: str # "improve", "analyze", "transform", "generate", "create", "edit" | |
| target_audience: Optional[str] = None | |
| tone: str = "neutral" | |
| scope: str = "full" | |
| domain: str = "general" | |
| constraints: List[str] = field(default_factory=list) | |
| reasoning: str = "" | |
| confidence: float = 0.0 | |
| needs_clarification: bool = False | |
| clarification_questions: List[str] = field(default_factory=list) | |
| extracted_entities: Dict[str, Any] = field(default_factory=dict) | |
| class EditPlan: | |
| """Execution plan for document editing""" | |
| strategy: str | |
| steps: List[str] = field(default_factory=list) | |
| constraints: List[str] = field(default_factory=list) | |
| target_metrics: Dict[str, Any] = field(default_factory=dict) | |
| rationale: str = "" | |
| estimated_tokens: int = 0 | |
| class DocumentProfile: | |
| """Comprehensive document analysis result""" | |
| structure: Dict[str, Any] = field(default_factory=dict) | |
| content: Dict[str, Any] = field(default_factory=dict) | |
| quality: Dict[str, Any] = field(default_factory=dict) | |
| suggestions: List[str] = field(default_factory=list) | |
| strengths: List[str] = field(default_factory=list) | |
| metadata: Dict[str, Any] = field(default_factory=dict) | |
| class EditResult: | |
| """Result of document edit operation""" | |
| edited_document: str = "" | |
| changes_made: Dict[str, Any] = field(default_factory=dict) | |
| reasoning: str = "" | |
| successful: bool = False | |
| streaming_complete: bool = True | |
| execution_time_ms: int = 0 | |
| class ConversationTurn: | |
| """Single conversation turn with context""" | |
| user_query: str = "" | |
| document_snapshot: Dict[str, Any] = field(default_factory=dict) | |
| assistant_response: str = "" | |
| edits_made: Dict[str, Any] = field(default_factory=dict) | |
| timestamp: datetime = field(default_factory=datetime.now) | |
| turn_id: str = field(default_factory=lambda: hashlib.md5(str(time.time()).encode()).hexdigest()[:8]) | |
| # ============================================================================ | |
| # DOCUMENT WORKSPACE CLASS | |
| # ============================================================================ | |
| class DocumentWorkspace: | |
| """Manages the active document with version control and change tracking""" | |
| def __init__(self): | |
| self.current_document = { | |
| "id": str(uuid.uuid4()), | |
| "title": "Untitled Document", | |
| "content": "", | |
| "type": "text", | |
| "created_at": datetime.now().isoformat(), | |
| "modified_at": datetime.now().isoformat(), | |
| "versions": [], | |
| "changes": [], | |
| "metadata": { | |
| "word_count": 0, | |
| "char_count": 0, | |
| "reading_time": 0, | |
| "style": "general", | |
| "language": "english" | |
| } | |
| } | |
| self.version_history = [] | |
| self.pending_changes = [] | |
| self.suggestion_mode = False | |
| self.track_changes = True | |
| def update_document(self, new_content, change_description=""): | |
| """Update document with change tracking""" | |
| old_content = self.current_document["content"] | |
| if old_content == new_content: | |
| return False | |
| if self.track_changes: | |
| self.save_version(f"Before: {change_description}") | |
| changes = self._calculate_changes(old_content, new_content) | |
| self.current_document["content"] = new_content | |
| self.current_document["modified_at"] = datetime.now().isoformat() | |
| self._update_metadata() | |
| change_record = { | |
| "id": len(self.current_document["changes"]), | |
| "timestamp": datetime.now().isoformat(), | |
| "description": change_description, | |
| "changes": changes, | |
| "type": "edit" | |
| } | |
| self.current_document["changes"].append(change_record) | |
| if self.track_changes: | |
| self.save_version(f"After: {change_description}") | |
| return True | |
| def _calculate_changes(self, old_text, new_text): | |
| """Calculate specific changes between versions""" | |
| changes = [] | |
| old_lines = old_text.split('\n') | |
| new_lines = new_text.split('\n') | |
| diff = list(unified_diff(old_lines, new_lines, lineterm='')) | |
| for line in diff: | |
| if line.startswith('+') and not line.startswith('+++'): | |
| changes.append({"type": "addition", "text": line[1:]}) | |
| elif line.startswith('-') and not line.startswith('---'): | |
| changes.append({"type": "deletion", "text": line[1:]}) | |
| return changes | |
| def _update_metadata(self): | |
| """Update document metadata""" | |
| content = self.current_document["content"] | |
| words = len(content.split()) | |
| chars = len(content) | |
| self.current_document["metadata"]["word_count"] = words | |
| self.current_document["metadata"]["char_count"] = chars | |
| self.current_document["metadata"]["reading_time"] = max(1, words // 200) | |
| def save_version(self, description=""): | |
| """Save current state as version""" | |
| version = { | |
| "id": len(self.version_history), | |
| "timestamp": datetime.now().isoformat(), | |
| "content": self.current_document["content"], | |
| "description": description, | |
| "metadata": self.current_document["metadata"].copy() | |
| } | |
| self.version_history.append(version) | |
| if len(self.version_history) > 50: | |
| self.version_history = self.version_history[-50:] | |
| return version | |
| def restore_version(self, version_id): | |
| """Restore a previous version""" | |
| if version_id < len(self.version_history): | |
| version = self.version_history[version_id] | |
| self.update_document(version["content"], f"Restored version {version_id}") | |
| return True | |
| return False | |
| def analyze_document(self): | |
| """Perform comprehensive document analysis""" | |
| content = self.current_document["content"] | |
| analysis = { | |
| "structure": self._analyze_structure(), | |
| "readability": self._analyze_readability(), | |
| "grammar_issues": self._check_grammar(), | |
| "style_analysis": self._analyze_style(), | |
| "suggestions": self._generate_suggestions() | |
| } | |
| return analysis | |
| def _analyze_structure(self): | |
| """Analyze document structure""" | |
| content = self.current_document["content"] | |
| lines = content.split('\n') | |
| headings = [] | |
| paragraphs = 0 | |
| lists = 0 | |
| for line in lines: | |
| if line.strip().startswith('#'): | |
| headings.append(line.strip()) | |
| elif len(line.strip()) > 20: | |
| paragraphs += 1 | |
| elif line.strip().startswith(('-', '*', '•')): | |
| lists += 1 | |
| return { | |
| "headings": headings, | |
| "paragraph_count": paragraphs, | |
| "list_items": lists, | |
| "total_lines": len(lines) | |
| } | |
| def _analyze_readability(self): | |
| """Calculate readability scores""" | |
| content = self.current_document["content"] | |
| sentences = re.split(r'[.!?]+', content) | |
| words = content.split() | |
| if len(sentences) == 0 or len(words) == 0: | |
| return {"score": 0, "level": "Unknown"} | |
| avg_words_per_sentence = len(words) / len(sentences) | |
| if avg_words_per_sentence < 10: | |
| score = 90 | |
| level = "Very Easy" | |
| elif avg_words_per_sentence < 15: | |
| score = 70 | |
| level = "Easy" | |
| elif avg_words_per_sentence < 20: | |
| score = 50 | |
| level = "Medium" | |
| elif avg_words_per_sentence < 25: | |
| score = 30 | |
| level = "Difficult" | |
| else: | |
| score = 10 | |
| level = "Very Difficult" | |
| return {"score": score, "level": level} | |
| def _check_grammar(self): | |
| """Basic grammar checking""" | |
| content = self.current_document["content"].lower() | |
| issues = [] | |
| common_errors = [ | |
| (r'\b(i)\s+(am|is|are|was|were)\s+(\w+ed)\b', "Passive voice detected"), | |
| (r'\b(very|really|quite|extremely)\s+(\w+)\b', "Consider removing intensifier"), | |
| (r'\b(there is|there are)\s+(\w+)\s+that\b', "Wordy construction"), | |
| ] | |
| for pattern, message in common_errors: | |
| if re.search(pattern, content): | |
| issues.append(message) | |
| return issues[:5] | |
| def _analyze_style(self): | |
| """Analyze writing style""" | |
| content = self.current_document["content"] | |
| style = "general" | |
| if re.search(r'\b(according to|citation|reference|study|research)\b', content, re.I): | |
| style = "academic" | |
| elif re.search(r'\b(proposal|budget|timeline|deliverable|stakeholder)\b', content, re.I): | |
| style = "business" | |
| elif re.search(r'\b(algorithm|function|class|import|def|return)\b', content): | |
| style = "technical" | |
| elif re.search(r'\b(chapter|scene|character|dialogue)\b', content, re.I): | |
| style = "creative" | |
| return {"detected_style": style, "confidence": 0.8} | |
| def _generate_suggestions(self): | |
| """Generate improvement suggestions""" | |
| content = self.current_document["content"] | |
| suggestions = [] | |
| if len(content.split()) < 100: | |
| suggestions.append("Consider expanding the document with more details") | |
| structure = self._analyze_structure() | |
| if len(structure["headings"]) == 0 and len(content) > 500: | |
| suggestions.append("Add headings to improve document structure") | |
| readability = self._analyze_readability() | |
| if readability["score"] < 30: | |
| suggestions.append("Simplify sentences to improve readability") | |
| return suggestions | |
| # ============================================================================ | |
| # 1. CONVERSATION MANAGER | |
| # ============================================================================ | |
| class ConversationManager: | |
| """Maintains persistent, multi-turn conversation memory with document state tracking""" | |
| def __init__(self, max_history: int = 10): | |
| self.max_history = max_history | |
| self.turns: List[ConversationTurn] = [] | |
| self.intent_summary: Optional[str] = None | |
| self.cumulative_edits: Dict[str, Any] = { | |
| "total_edits": 0, | |
| "sections_affected": defaultdict(int), | |
| "first_interaction": None, | |
| "last_interaction": None | |
| } | |
| def add_user_message(self, query: str, document_state: dict) -> None: | |
| snapshot = { | |
| "word_count": document_state.get("word_count", 0), | |
| "char_count": document_state.get("char_count", 0), | |
| "title": document_state.get("title", "Untitled"), | |
| "content_preview": document_state.get("content", "")[:200], | |
| "has_content": bool(document_state.get("content", "")) | |
| } | |
| turn = ConversationTurn( | |
| user_query=query, | |
| document_snapshot=snapshot, | |
| assistant_response="", | |
| edits_made={} | |
| ) | |
| self.turns.append(turn) | |
| if self.cumulative_edits["first_interaction"] is None: | |
| self.cumulative_edits["first_interaction"] = datetime.now() | |
| self.cumulative_edits["last_interaction"] = datetime.now() | |
| if len(self.turns) > self.max_history: | |
| self.turns = self.turns[-self.max_history:] | |
| def add_assistant_message(self, response: str, edits_made: dict) -> None: | |
| if self.turns: | |
| self.turns[-1].assistant_response = response | |
| self.turns[-1].edits_made = edits_made | |
| self.cumulative_edits["total_edits"] += 1 | |
| for section in edits_made.get("sections_modified", []): | |
| self.cumulative_edits["sections_affected"][section] += 1 | |
| def get_conversation_context(self) -> str: | |
| if not self.turns: | |
| return "No previous conversation." | |
| context_parts = ["## Conversation History\n"] | |
| for i, turn in enumerate(self.turns[-self.max_history:], 1): | |
| context_parts.append(f"**Turn {i}:**") | |
| context_parts.append(f"User: \"{turn.user_query[:200]}\"") | |
| context_parts.append(f"Document: {turn.document_snapshot.get('title', 'Untitled')} " | |
| f"({turn.document_snapshot.get('word_count', 0)} words)") | |
| if turn.edits_made: | |
| changes_desc = ", ".join(turn.edits_made.get("key_changes", [])[:3]) | |
| if changes_desc: | |
| context_parts.append(f"Result: {changes_desc}") | |
| context_parts.append("") | |
| if self.cumulative_edits["total_edits"] > 1: | |
| context_parts.append(f"**Cumulative:** {self.cumulative_edits['total_edits']} edits across " | |
| f"{len(self.cumulative_edits['sections_affected'])} sections") | |
| return "\n".join(context_parts) | |
| def get_document_evolution(self) -> List[Dict]: | |
| evolution = [] | |
| for turn in self.turns: | |
| evolution.append({ | |
| "turn_id": turn.turn_id, | |
| "timestamp": turn.timestamp.isoformat(), | |
| "query": turn.user_query[:100], | |
| "document_state": turn.document_snapshot, | |
| "changes": turn.edits_made | |
| }) | |
| return evolution | |
| def summarize_intent(self, llm_client=None) -> str: | |
| if not self.turns: | |
| return "No conversation to summarize" | |
| queries = [turn.user_query for turn in self.turns[-5:]] | |
| if llm_client and len(queries) > 1: | |
| try: | |
| prompt = f"""Based on these user queries about document editing, what is the user's OVERARCHING intent? | |
| Queries: | |
| {chr(10).join(f'- {q}' for q in queries)} | |
| Summarize in one sentence what the user is trying to achieve:""" | |
| messages = [{"role": "user", "content": prompt}] | |
| response = llm_client.chat.completions.create( | |
| model="llama-3.3-70b-versatile", | |
| messages=messages, | |
| max_tokens=100, | |
| temperature=0.3 | |
| ) | |
| self.intent_summary = response.choices[0].message.content.strip() | |
| return self.intent_summary | |
| except Exception: | |
| pass | |
| keywords = [] | |
| for q in queries: | |
| words = q.lower().split()[:5] | |
| keywords.extend(words) | |
| unique_keywords = list(set(keywords))[:5] | |
| self.intent_summary = f"User is focused on: {', '.join(unique_keywords)}" | |
| return self.intent_summary | |
| def clear(self) -> None: | |
| self.turns = [] | |
| self.intent_summary = None | |
| self.cumulative_edits = { | |
| "total_edits": 0, | |
| "sections_affected": defaultdict(int), | |
| "first_interaction": None, | |
| "last_interaction": None | |
| } | |
| # ============================================================================ | |
| # 2. INSTRUCTION PARSER | |
| # ============================================================================ | |
| class InstructionParser: | |
| """Extract semantic meaning from user instructions using AI""" | |
| def __init__(self, llm_client=None): | |
| self.llm_client = llm_client | |
| self.confidence_threshold = 0.7 | |
| def parse(self, instruction: str, document: dict) -> ParsedInstruction: | |
| if self.llm_client: | |
| try: | |
| return self._parse_with_ai(instruction, document) | |
| except Exception as e: | |
| print(f"AI parsing failed: {e}") | |
| return self._parse_with_regex(instruction, document) | |
| def _parse_with_ai(self, instruction: str, document: dict) -> ParsedInstruction: | |
| system_prompt = """You are an instruction parser for a document editing AI. Given a user instruction, extract semantic intent. | |
| Output ONLY valid JSON with this structure: | |
| { | |
| "intent": "improve|analyze|transform|generate|create|edit", | |
| "target_audience": "string or null", | |
| "tone": "formal|casual|academic|persuasive|neutral", | |
| "scope": "full|introduction|section|conclusion|paragraph", | |
| "domain": "business|academic|technical|creative|general", | |
| "constraints": ["list of specific requirements"], | |
| "confidence": 0.95, | |
| "reasoning": "Why this interpretation?", | |
| "needs_clarification": false, | |
| "clarification_questions": ["Question if needed?"], | |
| "extracted_entities": {"key": "value"} | |
| }""" | |
| user_prompt = f"""Instruction: "{instruction}" | |
| Document context: {document.get('title', 'Untitled')} ({document.get('word_count', 0)} words) | |
| Parse this instruction and include clarification_questions if the instruction is ambiguous (confidence < 0.7):""" | |
| try: | |
| messages = [ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt} | |
| ] | |
| response = self.llm_client.chat.completions.create( | |
| model="llama-3.3-70b-versatile", | |
| messages=messages, | |
| max_tokens=500, | |
| temperature=0.2 | |
| ) | |
| result_text = response.choices[0].message.content.strip() | |
| json_match = re.search(r'\{.*\}', result_text, re.DOTALL) | |
| if json_match: | |
| data = json.loads(json_match.group()) | |
| return ParsedInstruction( | |
| intent=data.get("intent", "edit"), | |
| target_audience=data.get("target_audience"), | |
| tone=data.get("tone", "neutral"), | |
| scope=data.get("scope", "full"), | |
| domain=data.get("domain", "general"), | |
| constraints=data.get("constraints", []), | |
| reasoning=data.get("reasoning", ""), | |
| confidence=data.get("confidence", 0.5), | |
| needs_clarification=data.get("needs_clarification", False), | |
| clarification_questions=data.get("clarification_questions", []), | |
| extracted_entities=data.get("extracted_entities", {}) | |
| ) | |
| except Exception as e: | |
| print(f"AI parsing error: {e}") | |
| return self._parse_with_regex(instruction, document) | |
| def _parse_with_regex(self, instruction: str, document: dict) -> ParsedInstruction: | |
| inst_lower = instruction.lower() | |
| intent = "edit" | |
| if any(word in inst_lower for word in ["improve", "enhance", "better"]): | |
| intent = "improve" | |
| elif any(word in inst_lower for word in ["analyze", "review", "check"]): | |
| intent = "analyze" | |
| elif any(word in inst_lower for word in ["transform", "convert", "change to"]): | |
| intent = "transform" | |
| elif any(word in inst_lower for word in ["generate", "create", "make"]): | |
| intent = "generate" | |
| tone = "neutral" | |
| if any(word in inst_lower for word in ["formal", "professional", "business"]): | |
| tone = "formal" | |
| elif any(word in inst_lower for word in ["casual", "friendly", "conversational"]): | |
| tone = "casual" | |
| elif any(word in inst_lower for word in ["academic", "scholarly", "research"]): | |
| tone = "academic" | |
| elif any(word in inst_lower for word in ["persuasive", "convincing", "compelling"]): | |
| tone = "persuasive" | |
| target_audience = None | |
| audience_patterns = [ | |
| (r"for\s+a\s+(\d+[\s-]*year[\s-]*old)", "child"), | |
| (r"for\s+(executives|leaders|managers)", "executive"), | |
| (r"for\s+(beginners|novices)", "beginner"), | |
| (r"for\s+(experts|professionals)", "expert"), | |
| ] | |
| for pattern, audience_type in audience_patterns: | |
| match = re.search(pattern, inst_lower) | |
| if match: | |
| target_audience = match.group(1) if match.groups() else audience_type | |
| break | |
| scope = "full" | |
| if "introduction" in inst_lower: | |
| scope = "introduction" | |
| elif "conclusion" in inst_lower: | |
| scope = "conclusion" | |
| elif re.search(r'section\s+(\d+)', inst_lower): | |
| scope = f"section:{re.search(r'section\s+(\d+)', inst_lower).group(1)}" | |
| constraints = [] | |
| word_match = re.search(r'under\s+(\d+)\s+words', inst_lower) | |
| if word_match: | |
| constraints.append(f"keep under {word_match.group(1)} words") | |
| if "keep accuracy" in inst_lower: | |
| constraints.append("preserve technical accuracy") | |
| confidence = 0.5 | |
| if intent != "edit": | |
| confidence += 0.1 | |
| if tone != "neutral": | |
| confidence += 0.1 | |
| if constraints: | |
| confidence += 0.1 | |
| confidence = min(confidence, 0.9) | |
| # FIX #9: Generate clarification questions when ambiguous | |
| clarification_questions = [] | |
| needs_clarification = confidence < self.confidence_threshold | |
| if needs_clarification: | |
| if tone == "neutral": | |
| clarification_questions.append("What tone should I use? (formal, casual, academic, persuasive)") | |
| if target_audience is None: | |
| clarification_questions.append("Who is the target audience for this document?") | |
| if scope == "full" and len(instruction.split()) < 5: | |
| clarification_questions.append("Should I edit the full document or a specific section?") | |
| if not constraints: | |
| clarification_questions.append("Are there any length or style constraints I should follow?") | |
| return ParsedInstruction( | |
| intent=intent, | |
| target_audience=target_audience, | |
| tone=tone, | |
| scope=scope, | |
| domain="general", | |
| constraints=constraints, | |
| reasoning="Parsed using pattern matching", | |
| confidence=confidence, | |
| needs_clarification=needs_clarification, | |
| clarification_questions=clarification_questions[:3] | |
| ) | |
| # ============================================================================ | |
| # 3. FILE CONTEXT ACCUMULATOR | |
| # ============================================================================ | |
| class FileContextAccumulator: | |
| """Remember all uploaded files and cross-reference them""" | |
| def __init__(self, llm_client=None): | |
| self.llm_client = llm_client | |
| self.files: Dict[str, Dict] = {} | |
| self.file_summaries: Dict[str, str] = {} | |
| self.semantic_index: Dict[str, List[str]] = defaultdict(list) | |
| def add_file(self, filename: str, file_type: str, content: str, metadata: dict) -> None: | |
| summary = self._generate_file_summary(filename, file_type, content, metadata) | |
| keywords = self._extract_keywords(content, metadata) | |
| self.files[filename] = { | |
| "filename": filename, | |
| "type": file_type, | |
| "content": content[:3000], | |
| "metadata": metadata, | |
| "summary": summary, | |
| "keywords": keywords, | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| self.file_summaries[filename] = summary | |
| for keyword in keywords: | |
| self.semantic_index[keyword].append(filename) | |
| def _generate_file_summary(self, filename: str, file_type: str, content: str, metadata: dict) -> str: | |
| if file_type == "csv": | |
| lines = content.strip().split('\n') | |
| if len(lines) > 1: | |
| headers = lines[0].split(',') | |
| return f"CSV with {len(lines)-1} data rows, columns: {', '.join(headers[:5])}" | |
| elif file_type == "json": | |
| try: | |
| data = json.loads(content[:1000]) | |
| if isinstance(data, dict): | |
| return f"JSON object with keys: {', '.join(list(data.keys())[:5])}" | |
| elif isinstance(data, list): | |
| return f"JSON array with {len(data)} items" | |
| except: | |
| pass | |
| words = len(content.split()) | |
| return f"File with {words} words. Type: {file_type}" | |
| def _extract_keywords(self, content: str, metadata: dict) -> List[str]: | |
| keywords = set() | |
| if "columns" in metadata: | |
| keywords.update(metadata["columns"]) | |
| words = content.lower().split()[:200] | |
| common_words = {"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for"} | |
| for word in words: | |
| if len(word) > 3 and word not in common_words: | |
| keywords.add(word) | |
| return list(keywords)[:20] | |
| def get_file_context(self) -> str: | |
| if not self.files: | |
| return "No files uploaded." | |
| context_parts = ["## Uploaded Files Context\n"] | |
| for filename, file_info in self.files.items(): | |
| context_parts.append(f"**File:** {filename}") | |
| context_parts.append(f"Type: {file_info['type']}") | |
| context_parts.append(f"Summary: {file_info['summary']}") | |
| context_parts.append("") | |
| return "\n".join(context_parts) | |
| def get_detailed_file_context(self, filename: str = None) -> str: | |
| if filename and filename in self.files: | |
| file_info = self.files[filename] | |
| return f"""## File: {filename} | |
| Type: {file_info['type']} | |
| Summary: {file_info['summary']} | |
| Content Preview: | |
| {file_info['content'][:500]} | |
| """ | |
| result = "" | |
| for filename, file_info in self.files.items(): | |
| result += f"\n### {filename}\n{file_info['summary']}\n" | |
| return result or "No files uploaded." | |
| def find_relevant_file(self, query: str) -> Optional[str]: | |
| query_lower = query.lower() | |
| best_match = None | |
| best_score = 0 | |
| for filename, file_info in self.files.items(): | |
| score = 0 | |
| for keyword in file_info["keywords"]: | |
| if keyword in query_lower: | |
| score += 1 | |
| if filename.lower() in query_lower: | |
| score += 2 | |
| if any(word in query_lower for word in file_info["summary"].lower().split()[:10]): | |
| score += 1 | |
| if score > best_score and score > 0: | |
| best_score = score | |
| best_match = filename | |
| return best_match | |
| def get_cross_references(self, document_content: str) -> List[Dict]: | |
| suggestions = [] | |
| for filename, file_info in self.files.items(): | |
| doc_lower = document_content.lower() | |
| file_keywords = file_info["keywords"][:5] | |
| matched_keywords = [kw for kw in file_keywords if kw in doc_lower] | |
| if matched_keywords: | |
| suggestions.append({ | |
| "file": filename, | |
| "type": file_info["type"], | |
| "matched_terms": matched_keywords, | |
| "suggestion": f"Reference data from {filename} regarding {', '.join(matched_keywords[:3])}" | |
| }) | |
| return suggestions | |
| def clear(self) -> None: | |
| self.files.clear() | |
| self.file_summaries.clear() | |
| self.semantic_index.clear() | |
| # ============================================================================ | |
| # 4. EDIT PLANNER | |
| # ============================================================================ | |
| class EditPlanner: | |
| """Plan document transformations before executing them""" | |
| def __init__(self, llm_client=None): | |
| self.llm_client = llm_client | |
| def plan(self, parsed: ParsedInstruction, document: dict, conversation=None) -> EditPlan: | |
| if self.llm_client: | |
| try: | |
| return self._plan_with_ai(parsed, document, conversation) | |
| except Exception as e: | |
| print(f"AI planning failed: {e}") | |
| return self._plan_with_templates(parsed, document, conversation) | |
| def _plan_with_ai(self, parsed: ParsedInstruction, document: dict, conversation) -> EditPlan: | |
| conv_context = "" | |
| if conversation: | |
| conv_context = conversation.get_conversation_context() | |
| system_prompt = """You are an edit planner for a document AI. Create a detailed execution plan. | |
| Output JSON: | |
| { | |
| "strategy": "Overall approach description", | |
| "steps": ["Step 1", "Step 2", "Step 3"], | |
| "constraints": ["Constraint 1", "Constraint 2"], | |
| "target_metrics": {"metric": "value"}, | |
| "rationale": "Why this approach" | |
| }""" | |
| user_prompt = f"""Parsed Instruction: | |
| - Intent: {parsed.intent} | |
| - Audience: {parsed.target_audience} | |
| - Tone: {parsed.tone} | |
| - Scope: {parsed.scope} | |
| Document: {document.get('title', 'Untitled')} ({document.get('word_count', 0)} words) | |
| {conv_context} | |
| Create edit plan:""" | |
| try: | |
| messages = [ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt} | |
| ] | |
| response = self.llm_client.chat.completions.create( | |
| model="llama-3.3-70b-versatile", | |
| messages=messages, | |
| max_tokens=800, | |
| temperature=0.4 | |
| ) | |
| result_text = response.choices[0].message.content.strip() | |
| json_match = re.search(r'\{.*\}', result_text, re.DOTALL) | |
| if json_match: | |
| data = json.loads(json_match.group()) | |
| return EditPlan( | |
| strategy=data.get("strategy", "Apply requested edits"), | |
| steps=data.get("steps", ["Analyze document", "Apply changes", "Verify result"]), | |
| constraints=data.get("constraints", parsed.constraints), | |
| target_metrics=data.get("target_metrics", {}), | |
| rationale=data.get("rationale", "Based on user instruction") | |
| ) | |
| except Exception as e: | |
| print(f"AI planning error: {e}") | |
| return self._plan_with_templates(parsed, document, conversation) | |
| def _plan_with_templates(self, parsed: ParsedInstruction, document: dict, conversation) -> EditPlan: | |
| intent_plans = { | |
| "improve": { | |
| "strategy": "Enhance document quality by improving clarity, flow, and engagement", | |
| "steps": ["Identify areas needing improvement", "Rewrite for better clarity", "Enhance vocabulary", "Ensure consistent tone"] | |
| }, | |
| "analyze": { | |
| "strategy": "Perform comprehensive document analysis without modifying content", | |
| "steps": ["Analyze document structure", "Evaluate content quality", "Check for grammar issues", "Generate recommendations"] | |
| }, | |
| "transform": { | |
| "strategy": "Transform document style and tone according to requirements", | |
| "steps": ["Understand target style", "Rewrite to match desired tone", "Adjust vocabulary", "Preserve core meaning"] | |
| }, | |
| "generate": { | |
| "strategy": "Generate new content based on document context", | |
| "steps": ["Analyze existing content", "Identify gaps", "Generate relevant content", "Integrate smoothly"] | |
| } | |
| } | |
| plan_template = intent_plans.get(parsed.intent, intent_plans["improve"]) | |
| steps = plan_template["steps"].copy() | |
| if parsed.tone != "neutral": | |
| steps.append(f"Adjust content to {parsed.tone} tone") | |
| if parsed.scope != "full": | |
| steps.insert(1, f"Focus exclusively on {parsed.scope} section") | |
| constraints = parsed.constraints.copy() | |
| if parsed.target_audience: | |
| constraints.append(f"Target audience: {parsed.target_audience}") | |
| return EditPlan( | |
| strategy=plan_template["strategy"], | |
| steps=steps, | |
| constraints=constraints, | |
| target_metrics={"preserve_facts": True}, | |
| rationale=f"Template-based plan for {parsed.intent} operation" | |
| ) | |
| # ============================================================================ | |
| # 5. STREAMING RESPONSE HANDLER | |
| # ============================================================================ | |
| class StreamingResponseHandler: | |
| """Stream responses token-by-token instead of blocking""" | |
| def __init__(self, client): | |
| self.client = client | |
| # Item 1: Expose last-run speed metrics for UI display | |
| self.last_metrics: Dict[str, Any] = {} | |
| def stream_completion( | |
| self, | |
| messages: List[Dict], | |
| on_token: Optional[Callable[[str], None]] = None, | |
| model: str = "llama-3.3-70b-versatile", | |
| temperature: float = 0.3, | |
| max_tokens: int = 4000 | |
| ) -> str: | |
| full_content = "" | |
| token_count = 0 | |
| start_time = time.time() | |
| first_token_time: Optional[float] = None | |
| try: | |
| stream = self.client.chat.completions.create( | |
| model=model, | |
| messages=messages, | |
| temperature=temperature, | |
| max_tokens=max_tokens, | |
| stream=True, | |
| timeout=60 | |
| ) | |
| for chunk in stream: | |
| if chunk.choices and chunk.choices[0].delta.content: | |
| token = chunk.choices[0].delta.content | |
| if first_token_time is None: | |
| first_token_time = time.time() | |
| full_content += token | |
| token_count += 1 | |
| if on_token: | |
| on_token(token) | |
| elapsed_ms = int((time.time() - start_time) * 1000) | |
| ttft_ms = int((first_token_time - start_time) * 1000) if first_token_time else 0 | |
| tokens_per_sec = round(token_count / max(elapsed_ms / 1000, 0.001), 1) | |
| # Item 1: Store metrics for caller to display | |
| self.last_metrics = { | |
| "elapsed_ms": elapsed_ms, | |
| "ttft_ms": ttft_ms, | |
| "token_count": token_count, | |
| "tokens_per_sec": tokens_per_sec, | |
| "char_count": len(full_content), | |
| } | |
| print(f"Streaming: {token_count} tokens, {tokens_per_sec} tok/s, TTFT {ttft_ms}ms") | |
| return full_content | |
| except Exception as e: | |
| print(f"Streaming error: {e}") | |
| try: | |
| response = self.client.chat.completions.create( | |
| model=model, | |
| messages=messages, | |
| temperature=temperature, | |
| max_tokens=max_tokens, | |
| stream=False | |
| ) | |
| full_content = response.choices[0].message.content.strip() | |
| self.last_metrics = {"elapsed_ms": 0, "ttft_ms": 0, "token_count": 0, | |
| "tokens_per_sec": 0, "char_count": len(full_content)} | |
| if on_token: | |
| on_token(full_content) | |
| return full_content | |
| except Exception as e2: | |
| print(f"Fallback failed: {e2}") | |
| return f"Error: {str(e)}" | |
| # ============================================================================ | |
| # 6. DOCUMENT PROFILER | |
| # ============================================================================ | |
| class DocumentProfiler: | |
| """Deep AI-driven analysis of document (replaces regex analysis)""" | |
| def __init__(self, llm_client=None): | |
| self.llm_client = llm_client | |
| def profile(self, document: dict) -> DocumentProfile: | |
| content = document.get("content", "") | |
| if not content or len(content.strip()) < 50: | |
| return self._empty_profile("Document too short for analysis") | |
| if self.llm_client: | |
| try: | |
| return self._profile_with_ai(document) | |
| except Exception as e: | |
| print(f"AI profiling failed: {e}") | |
| return self._profile_with_fallback(document) | |
| def _profile_with_ai(self, document: dict) -> DocumentProfile: | |
| content = document.get("content", "") | |
| title = document.get("title", "Untitled") | |
| system_prompt = """You are a document profiler. Analyze the document and output JSON. | |
| Output format: | |
| { | |
| "structure": { | |
| "has_clear_intro": true/false, | |
| "has_body_paragraphs": true/false, | |
| "has_conclusion": true/false, | |
| "logical_flow": "good|fair|poor", | |
| "issues": ["specific structural issues"] | |
| }, | |
| "content": { | |
| "primary_purpose": "inform|persuade|entertain|instruct", | |
| "target_audience": "inferred audience description", | |
| "tone": "formal|casual|academic|persuasive", | |
| "reading_level": "elementary|high_school|college|expert" | |
| }, | |
| "quality": { | |
| "grammar_issues": ["specific grammar issues"], | |
| "clarity_problems": ["unclear sections"], | |
| "engagement_score": 0.0-1.0 | |
| }, | |
| "suggestions": ["specific, actionable suggestion 1", "suggestion 2"], | |
| "strengths": ["strength 1", "strength 2"] | |
| }""" | |
| user_prompt = f"""Title: {title} | |
| Content: | |
| {content[:3000]} | |
| Analyze this document:""" | |
| try: | |
| messages = [ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt} | |
| ] | |
| response = self.llm_client.chat.completions.create( | |
| model="llama-3.3-70b-versatile", | |
| messages=messages, | |
| max_tokens=1000, | |
| temperature=0.3 | |
| ) | |
| result_text = response.choices[0].message.content.strip() | |
| json_match = re.search(r'\{.*\}', result_text, re.DOTALL) | |
| if json_match: | |
| data = json.loads(json_match.group()) | |
| return DocumentProfile( | |
| structure=data.get("structure", {}), | |
| content=data.get("content", {}), | |
| quality=data.get("quality", {}), | |
| suggestions=data.get("suggestions", []), | |
| strengths=data.get("strengths", []), | |
| metadata={"analyzed_by": "AI", "timestamp": datetime.now().isoformat()} | |
| ) | |
| except Exception as e: | |
| print(f"AI profile error: {e}") | |
| return self._profile_with_fallback(document) | |
| def _profile_with_fallback(self, document: dict) -> DocumentProfile: | |
| content = document.get("content", "") | |
| has_intro = any(word in content[:500].lower() for word in ["introduction", "overview"]) | |
| has_conclusion = any(word in content[-500:].lower() for word in ["conclusion", "summary"]) | |
| paragraphs = [p for p in content.split('\n\n') if len(p.strip()) > 50] | |
| has_body = len(paragraphs) >= 2 | |
| grammar_issues = [] | |
| passive_matches = re.findall(r'\b(?:is|are|was|were|be|been|being)\s+\w+ed\b', content, re.I) | |
| if len(passive_matches) > 5: | |
| grammar_issues.append("Excessive passive voice usage") | |
| long_sentences = [s for s in re.split(r'[.!?]+', content) if len(s.split()) > 30] | |
| if len(long_sentences) > 3: | |
| grammar_issues.append(f"{len(long_sentences)} very long sentences") | |
| suggestions = [] | |
| if not has_intro: | |
| suggestions.append("Add a clear introduction") | |
| if not has_conclusion: | |
| suggestions.append("Add a conclusion to summarize key points") | |
| if len(content.split()) < 200: | |
| suggestions.append("Consider expanding with more details") | |
| tone = "neutral" | |
| formal_count = sum(content.lower().count(w) for w in ["therefore", "consequently"]) | |
| casual_count = sum(content.lower().count(w) for w in ["basically", "actually"]) | |
| if formal_count > casual_count * 2: | |
| tone = "formal" | |
| elif casual_count > formal_count * 2: | |
| tone = "casual" | |
| return DocumentProfile( | |
| structure={ | |
| "has_clear_intro": has_intro, | |
| "has_body_paragraphs": has_body, | |
| "has_conclusion": has_conclusion, | |
| "logical_flow": "fair" if has_intro and has_body else "poor", | |
| "issues": [] | |
| }, | |
| content={ | |
| "primary_purpose": "inform", | |
| "target_audience": "general audience", | |
| "tone": tone, | |
| "reading_level": "college" if len(content.split()) > 500 else "high_school" | |
| }, | |
| quality={ | |
| "grammar_issues": grammar_issues[:3], | |
| "clarity_problems": [], | |
| "engagement_score": 0.5 | |
| }, | |
| suggestions=suggestions[:5], | |
| strengths=[], | |
| metadata={"analyzed_by": "fallback"} | |
| ) | |
| def _empty_profile(self, reason: str) -> DocumentProfile: | |
| return DocumentProfile( | |
| structure={"has_clear_intro": False, "has_conclusion": False, "logical_flow": "poor", "issues": [reason]}, | |
| content={"primary_purpose": "unknown", "target_audience": "unknown", "tone": "neutral", "reading_level": "unknown"}, | |
| quality={"grammar_issues": [], "clarity_problems": [], "engagement_score": 0.0}, | |
| suggestions=["Add more content for proper analysis"], | |
| strengths=[], | |
| metadata={"error": reason} | |
| ) | |
| # ============================================================================ | |
| # 7. CONTEXTUAL EDITOR (Core Edit Engine) | |
| # ============================================================================ | |
| class ContextualEditor: | |
| """Execute document edits using full context - orchestrates all components""" | |
| def __init__(self, llm_client, streaming_handler: StreamingResponseHandler): | |
| self.llm_client = llm_client | |
| self.streaming_handler = streaming_handler | |
| self.instruction_parser = InstructionParser(llm_client) | |
| self.edit_planner = EditPlanner(llm_client) | |
| def edit( | |
| self, | |
| instruction: str, | |
| document: dict, | |
| conversation: Optional[ConversationManager] = None, | |
| file_context: Optional[FileContextAccumulator] = None, | |
| stream_callback: Optional[Callable[[str], None]] = None, | |
| selected_files: Optional[List[str]] = None, # Item 3: multi-file selection | |
| ) -> EditResult: | |
| start_time = time.time() | |
| parsed = self.instruction_parser.parse(instruction, document) | |
| if parsed.needs_clarification and parsed.confidence < 0.6: | |
| return EditResult( | |
| edited_document=document.get("content", ""), | |
| changes_made={}, | |
| reasoning=f"Need clarification", | |
| successful=False, | |
| execution_time_ms=int((time.time() - start_time) * 1000) | |
| ) | |
| conv_context = "" | |
| if conversation: | |
| conv_context = conversation.get_conversation_context() | |
| file_context_str = "" | |
| if file_context and file_context.files: | |
| # Item 2: Auto-reference files — inject content of matched/selected files | |
| if selected_files: | |
| # User explicitly chose files (Item 3) | |
| for fname in selected_files: | |
| detail = file_context.get_detailed_file_context(fname) | |
| file_context_str += detail + "\n" | |
| else: | |
| # Auto-detect the most relevant file | |
| relevant_file = file_context.find_relevant_file(instruction) | |
| if relevant_file: | |
| # Inject summary + content preview so AI can actually use the data | |
| file_context_str = file_context.get_detailed_file_context(relevant_file) | |
| else: | |
| file_context_str = file_context.get_file_context() | |
| plan = self.edit_planner.plan(parsed, document, conversation) | |
| prompt = self._build_edit_prompt( | |
| instruction=instruction, | |
| parsed=parsed, | |
| plan=plan, | |
| document=document, | |
| conv_context=conv_context, | |
| file_context=file_context_str | |
| ) | |
| messages = [ | |
| {"role": "system", "content": self._get_system_prompt(conversation)}, | |
| {"role": "user", "content": prompt} | |
| ] | |
| try: | |
| if stream_callback: | |
| edited_content = self.streaming_handler.stream_completion( | |
| messages=messages, | |
| on_token=stream_callback, | |
| model="llama-3.3-70b-versatile", | |
| max_tokens=4000 | |
| ) | |
| else: | |
| response = self.llm_client.chat.completions.create( | |
| model="llama-3.3-70b-versatile", | |
| messages=messages, | |
| temperature=0.3, | |
| max_tokens=4000 | |
| ) | |
| edited_content = response.choices[0].message.content.strip() | |
| changes_made = self._calculate_changes( | |
| document.get("content", ""), | |
| edited_content, | |
| plan | |
| ) | |
| execution_ms = int((time.time() - start_time) * 1000) | |
| return EditResult( | |
| edited_document=edited_content, | |
| changes_made=changes_made, | |
| reasoning=plan.rationale, | |
| successful=True, | |
| execution_time_ms=execution_ms | |
| ) | |
| except Exception as e: | |
| return EditResult( | |
| edited_document=document.get("content", ""), | |
| changes_made={}, | |
| reasoning=f"Edit failed: {str(e)}", | |
| successful=False, | |
| execution_time_ms=int((time.time() - start_time) * 1000) | |
| ) | |
| def _build_edit_prompt(self, instruction, parsed, plan, document, conv_context, file_context): | |
| content_preview = document.get("content", "") | |
| if len(content_preview) > 4000: | |
| content_preview = content_preview[:4000] + "\n...[truncated]..." | |
| prompt_parts = [ | |
| "## EDIT INSTRUCTION", | |
| f"User: {instruction}", | |
| "", | |
| "## PARSED INTENT", | |
| f"- Intent: {parsed.intent}", | |
| f"- Tone: {parsed.tone}", | |
| f"- Audience: {parsed.target_audience or 'Not specified'}", | |
| f"- Scope: {parsed.scope}", | |
| f"- Constraints: {', '.join(parsed.constraints) if parsed.constraints else 'None'}", | |
| "", | |
| "## EDIT PLAN", | |
| f"Strategy: {plan.strategy}", | |
| f"Steps:", | |
| ] | |
| for step in plan.steps: | |
| prompt_parts.append(f" {step}") | |
| # FIX #5: Make conversation context PROMINENT at top of prompt | |
| if conv_context and conv_context != "No previous conversation.": | |
| prompt_parts.extend([ | |
| "", | |
| "## ⚠️ CRITICAL - CONVERSATION CONTEXT (MUST FOLLOW)", | |
| conv_context, | |
| "IMPORTANT: The above history shows what was done previously.", | |
| "You MUST maintain any tone/style/constraints established in previous turns.", | |
| ]) | |
| if file_context and file_context != "No files uploaded.": | |
| prompt_parts.extend(["", file_context]) | |
| prompt_parts.extend([ | |
| "", | |
| "## DOCUMENT TO EDIT", | |
| "```", | |
| content_preview, | |
| "```", | |
| "", | |
| "Return ONLY the edited document content." | |
| ]) | |
| return "\n".join(prompt_parts) | |
| def _get_system_prompt(self, conversation=None) -> str: | |
| # FIX #5: Inject cumulative intent into system prompt | |
| intent_note = "" | |
| if conversation and conversation.intent_summary: | |
| intent_note = f"\nUser's overarching goal: {conversation.intent_summary}\nMaintain this goal across all edits." | |
| return f"""You are MozeAI Document Editor, a precise document editing AI.{intent_note} | |
| Return ONLY the edited document content - no explanations, no chat responses. | |
| Preserve the original meaning unless instructed otherwise. | |
| Apply changes exactly as described. | |
| ALWAYS maintain any tone, style, or constraints established in previous conversation turns.""" | |
| def _calculate_changes(self, old_content: str, new_content: str, plan: EditPlan) -> Dict: | |
| old_words = len(old_content.split()) | |
| new_words = len(new_content.split()) | |
| word_diff = new_words - old_words | |
| return { | |
| "additions": max(0, word_diff), | |
| "deletions": max(0, -word_diff), | |
| "net_change": word_diff, | |
| "old_word_count": old_words, | |
| "new_word_count": new_words, | |
| "sections_affected": ["content"], | |
| "key_changes": [f"Word count: {word_diff:+d} words ({old_words} → {new_words})"] | |
| } | |
| # ============================================================================ | |
| # FILE PROCESSING FUNCTIONS | |
| # ============================================================================ | |
| def extract_text_from_pdf(file): | |
| try: | |
| file.seek(0) | |
| pdf_reader = PyPDF2.PdfReader(file) | |
| text = "" | |
| for page_num, page in enumerate(pdf_reader.pages): | |
| page_text = page.extract_text() | |
| if page_text and page_text.strip(): | |
| text += f"\n--- Page {page_num + 1} ---\n" | |
| text += page_text.strip() + "\n" | |
| return text[:5000] if text.strip() else "No extractable text in PDF" | |
| except Exception as e: | |
| return f"Error reading PDF: {str(e)}" | |
| def extract_text_from_docx(file): | |
| try: | |
| file.seek(0) | |
| doc = docx.Document(file) | |
| text = "" | |
| for para in doc.paragraphs: | |
| if para.text and para.text.strip(): | |
| text += para.text.strip() + "\n\n" | |
| return text[:5000] if text.strip() else "No extractable text in document" | |
| except Exception as e: | |
| return f"Error reading Word document: {str(e)}" | |
| def extract_text_from_txt(file): | |
| try: | |
| file.seek(0) | |
| content = file.read().decode('utf-8') | |
| return content[:5000] if content.strip() else "File is empty" | |
| except UnicodeDecodeError: | |
| try: | |
| file.seek(0) | |
| content = file.read().decode('latin-1') | |
| return content[:5000] | |
| except: | |
| return "Error decoding text file" | |
| except Exception as e: | |
| return f"Error reading text file: {str(e)}" | |
| def extract_text_from_csv(file): | |
| try: | |
| file.seek(0) | |
| content = file.read().decode('utf-8') | |
| csv_reader = csv.reader(StringIO(content)) | |
| text = "CSV Data:\n\n" | |
| rows = list(csv_reader) | |
| if rows: | |
| text += "Headers: " + " | ".join(rows[0]) + "\n\n" | |
| for i, row in enumerate(rows[1:11], 1): | |
| text += f"Row {i}: " + " | ".join(row) + "\n" | |
| return text[:5000] if text.strip() else "CSV file appears empty" | |
| except Exception as e: | |
| return f"Error reading CSV: {str(e)}" | |
| def extract_text_from_json(file): | |
| try: | |
| file.seek(0) | |
| content = file.read().decode('utf-8') | |
| data = json.loads(content) | |
| formatted = json.dumps(data, indent=2) | |
| return formatted[:5000] if formatted else "JSON file is empty" | |
| except Exception as e: | |
| return f"Error reading JSON: {str(e)}" | |
| def process_uploaded_file(uploaded_file): | |
| file_type = uploaded_file.type | |
| file_name = uploaded_file.name.lower() | |
| if file_type == "application/pdf" or file_name.endswith('.pdf'): | |
| return extract_text_from_pdf(uploaded_file) | |
| elif file_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" or file_name.endswith('.docx'): | |
| return extract_text_from_docx(uploaded_file) | |
| elif file_type == "text/plain" or file_name.endswith('.txt'): | |
| return extract_text_from_txt(uploaded_file) | |
| elif file_type == "text/csv" or file_name.endswith('.csv'): | |
| return extract_text_from_csv(uploaded_file) | |
| elif file_type == "application/json" or file_name.endswith('.json'): | |
| return extract_text_from_json(uploaded_file) | |
| else: | |
| return f"Unsupported file type: {file_type}" | |
| # ============================================================================ | |
| # DOCUMENT GENERATION FUNCTIONS | |
| # ============================================================================ | |
| def create_ppt_from_content(title, content, filename="presentation"): | |
| try: | |
| prs = Presentation() | |
| title_slide_layout = prs.slide_layouts[0] | |
| slide = prs.slides.add_slide(title_slide_layout) | |
| slide.shapes.title.text = title[:100] | |
| slide.placeholders[1].text = f"Created by MozeAI\n{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" | |
| content_slide_layout = prs.slide_layouts[1] | |
| lines = content.split('\n') | |
| current_slide = None | |
| current_text_frame = None | |
| for line in lines: | |
| line = line.strip() | |
| if not line: | |
| continue | |
| if len(line) < 60 and (line.endswith(':') or line.isupper() or re.match(r'^\d+\.', line)): | |
| current_slide = prs.slides.add_slide(content_slide_layout) | |
| current_slide.shapes.title.text = line.rstrip(':')[:100] | |
| content_box = current_slide.placeholders[1] | |
| current_text_frame = content_box.text_frame | |
| current_text_frame.text = "" | |
| else: | |
| if current_slide is None: | |
| current_slide = prs.slides.add_slide(content_slide_layout) | |
| current_slide.shapes.title.text = "Content" | |
| content_box = current_slide.placeholders[1] | |
| current_text_frame = content_box.text_frame | |
| current_text_frame.text = "" | |
| if current_text_frame: | |
| p = current_text_frame.add_paragraph() | |
| p.text = line[:150] | |
| p.font.size = Pt(18) | |
| ppt_bytes = BytesIO() | |
| prs.save(ppt_bytes) | |
| ppt_bytes.seek(0) | |
| return ppt_bytes | |
| except Exception as e: | |
| print(f"PPT error: {e}") | |
| return None | |
| def create_word_from_content(title, content, filename="document"): | |
| try: | |
| doc = WordDocument() | |
| title_heading = doc.add_heading(title, 0) | |
| title_heading.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| doc.add_paragraph(f"Generated by MozeAI on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") | |
| doc.add_paragraph() | |
| paragraphs = content.split('\n\n') | |
| for para in paragraphs: | |
| if para.strip(): | |
| doc.add_paragraph(para.strip()) | |
| word_bytes = BytesIO() | |
| doc.save(word_bytes) | |
| word_bytes.seek(0) | |
| return word_bytes | |
| except Exception as e: | |
| return None | |
| def create_real_excel_file(title, data_rows): | |
| try: | |
| from openpyxl import Workbook | |
| from openpyxl.styles import Font, PatternFill, Alignment | |
| from openpyxl.utils import get_column_letter | |
| wb = Workbook() | |
| ws = wb.active | |
| ws.title = title[:31].replace('/', '_') | |
| for row_idx, row in enumerate(data_rows, 1): | |
| for col_idx, value in enumerate(row, 1): | |
| cell = ws.cell(row=row_idx, column=col_idx, value=value) | |
| if row_idx == 1: | |
| cell.font = Font(bold=True, color="FFFFFF") | |
| cell.fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") | |
| for col in ws.columns: | |
| max_length = 0 | |
| for cell in col: | |
| try: | |
| if len(str(cell.value)) > max_length: | |
| max_length = len(str(cell.value)) | |
| except: | |
| pass | |
| ws.column_dimensions[get_column_letter(col[0].column)].width = min(max_length + 2, 50) | |
| output = BytesIO() | |
| wb.save(output) | |
| output.seek(0) | |
| return output | |
| except Exception as e: | |
| print(f"Excel error: {e}") | |
| return None | |
| def create_csv_from_data(title, data_rows): | |
| try: | |
| output = BytesIO() | |
| output.write('\ufeff'.encode('utf-8')) | |
| writer = csv.writer(output) | |
| for row in data_rows: | |
| writer.writerow(row) | |
| output.seek(0) | |
| return output | |
| except Exception as e: | |
| return None | |
| def export_chat_history(): | |
| """FIX #10: Enhanced export includes intent + timeline""" | |
| if not st.session_state.chat_history: | |
| return None | |
| export_content = "=" * 70 + "\n" | |
| export_content += "CHAT HISTORY WITH MOZEAI\n" | |
| export_content += f"Exported on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" | |
| export_content += "=" * 70 + "\n\n" | |
| # Include intent summary | |
| core = st.session_state.get("intelligence_core") | |
| if core: | |
| conv_manager = core["conversation_manager"] | |
| if conv_manager.intent_summary: | |
| export_content += f"=== SESSION INTENT ===\n{conv_manager.intent_summary}\n\n" | |
| evolution = conv_manager.get_document_evolution() | |
| if evolution: | |
| export_content += "=== EDIT TIMELINE ===\n" | |
| for i, turn in enumerate(evolution, 1): | |
| changes = turn.get("changes", {}) | |
| net = changes.get("net_change", 0) | |
| export_content += f"{i}. {turn['query'][:60]}... → {net:+d} words\n" | |
| export_content += "\n" | |
| export_content += "=== CONVERSATION ===\n\n" | |
| for idx, (role, msg) in enumerate(st.session_state.chat_history, 1): | |
| if role == "user": | |
| export_content += f"[{idx}] USER:\n{msg}\n\n" | |
| else: | |
| export_content += f"[{idx}] MOZEAI:\n{msg}\n\n" | |
| return export_content | |
| # ============================================================================ | |
| # WEB & UTILITY FUNCTIONS | |
| # ============================================================================ | |
| def get_current_datetime(): | |
| tz = pytz.timezone('Asia/Seoul') | |
| now = datetime.now(tz) | |
| return f"Date: {now.strftime('%B %d, %Y')}\nTime: {now.strftime('%I:%M %p')}\nTimezone: Asia/Seoul" | |
| def internet_search(query): | |
| try: | |
| clean_query = query.strip() | |
| url = "https://html.duckduckgo.com/html/" | |
| params = {"q": clean_query} | |
| headers = {"User-Agent": "Mozilla/5.0"} | |
| response = requests.post(url, data=params, headers=headers, timeout=10) | |
| if response.status_code == 200: | |
| results = re.findall(r'<a rel="nofollow" class="result__a" href="[^"]*">([^<]+)</a>', response.text) | |
| snippets = re.findall(r'<a class="result__snippet"[^>]*>([^<]+)</a>', response.text) | |
| if results: | |
| context = f"SEARCH RESULTS for '{clean_query}':\n\n" | |
| for i in range(min(3, len(results))): | |
| context += f"- {results[i]}\n" | |
| if i < len(snippets): | |
| snippet = re.sub(r'<[^>]+>', '', snippets[i]) | |
| context += f" {snippet[:300]}...\n\n" | |
| return context[:2000] | |
| return "" | |
| except: | |
| return "" | |
| def generate_image_with_quality(prompt, quality="high", style="realistic"): | |
| try: | |
| enhanced_prompt = f"{prompt}, high quality, detailed" | |
| encoded_prompt = requests.utils.quote(enhanced_prompt) | |
| timestamp = int(time.time()) | |
| image_url = f"https://image.pollinations.ai/prompt/{encoded_prompt}?width=1024&height=1024&seed={timestamp}" | |
| return image_url | |
| except Exception as e: | |
| return None | |
| def generate_and_display_image(prompt, is_edit=False): | |
| image_url = generate_image_with_quality(prompt) | |
| if image_url: | |
| return f"Generated Image for: '{prompt}'\n\n" | |
| else: | |
| return "Sorry, I couldn't generate an image." | |
| def llm_with_fallback(messages, max_retries=2): | |
| models_to_try = [ | |
| "llama-3.3-70b-versatile", | |
| "llama-3.1-70b-versatile", | |
| "mixtral-8x7b-32768" | |
| ] | |
| for model in models_to_try: | |
| for attempt in range(max_retries): | |
| try: | |
| completion = client.chat.completions.create( | |
| model=model, | |
| temperature=0.3, | |
| max_tokens=800, | |
| messages=messages, | |
| timeout=30 | |
| ) | |
| st.session_state.last_model_used = model | |
| return completion.choices[0].message.content.strip() | |
| except Exception as e: | |
| if attempt < max_retries - 1: | |
| time.sleep(2 ** attempt) | |
| continue | |
| return "AI service temporarily unavailable." | |
| def reason(question, context): | |
| messages = [ | |
| {"role": "system", "content": "You are MozeAI, a helpful AI assistant."}, | |
| {"role": "user", "content": f"{context}\n\nUSER QUESTION: {question}\n\nANSWER:"} | |
| ] | |
| return llm_with_fallback(messages) | |
| def clean_answer(text): | |
| text = text.split("🧠")[0] | |
| text = text.split("Plan:")[0] | |
| return text.strip() | |
| # ============================================================================ | |
| # ENHANCED RUN AGENT WITH INTELLIGENCE CORE | |
| # ============================================================================ | |
| def handle_intelligent_edit(instruction: str, stream_callback=None, selected_files=None): | |
| """Use contextual editor for intelligent document editing""" | |
| core = st.session_state.intelligence_core | |
| document_state = { | |
| "content": st.session_state.workspace.current_document["content"], | |
| "title": st.session_state.workspace.current_document["title"], | |
| "word_count": len(st.session_state.workspace.current_document["content"].split()), | |
| "char_count": len(st.session_state.workspace.current_document["content"]) | |
| } | |
| # Check clarification BEFORE editing | |
| parser = core["instruction_parser"] | |
| parsed = parser.parse(instruction, document_state) | |
| if parsed.needs_clarification and parsed.confidence < 0.6 and parsed.clarification_questions: | |
| st.session_state.pending_clarification = { | |
| "instruction": instruction, | |
| "questions": parsed.clarification_questions, | |
| "parsed": parsed | |
| } | |
| return None # Signal that clarification is needed | |
| result = core["contextual_editor"].edit( | |
| instruction=instruction, | |
| document=document_state, | |
| conversation=core["conversation_manager"], | |
| file_context=core["file_accumulator"], | |
| stream_callback=stream_callback, | |
| selected_files=selected_files, # Item 2 & 3: pass through | |
| ) | |
| if result.successful: | |
| core["conversation_manager"].add_user_message(instruction, document_state) | |
| core["conversation_manager"].add_assistant_message("Document edited", result.changes_made) | |
| st.session_state.workspace.update_document(result.edited_document, f"AI Edit: {instruction[:100]}") | |
| return result | |
| def profile_current_document(): | |
| """Profile current document using AI""" | |
| core = st.session_state.intelligence_core | |
| document_state = { | |
| "content": st.session_state.workspace.current_document["content"], | |
| "title": st.session_state.workspace.current_document["title"] | |
| } | |
| return core["document_profiler"].profile(document_state) | |
| def run_agent(query: str, stream_callback=None, selected_files=None): | |
| """Enhanced agent with intelligence core integration""" | |
| q = query.lower().strip() | |
| # Document workspace commands | |
| if q.startswith("/"): | |
| return handle_document_command(q) | |
| # Check for document editing commands | |
| edit_keywords = ["improve", "rewrite", "summarize", "expand", "shorten", | |
| "fix grammar", "make formal", "make academic", "translate"] | |
| if any(keyword in q for keyword in edit_keywords) and st.session_state.workspace.current_document["content"]: | |
| # Item 6: Edit validation — snapshot word count before edit | |
| pre_word_count = len(st.session_state.workspace.current_document["content"].split()) | |
| result = handle_intelligent_edit(query, stream_callback=stream_callback, selected_files=selected_files) | |
| # Clarification needed | |
| if result is None: | |
| return "__CLARIFICATION_NEEDED__" | |
| if result.successful: | |
| # Item 6: Validate edit didn't produce empty/trivially-short output | |
| post_word_count = len(result.edited_document.split()) | |
| if post_word_count < max(10, pre_word_count * 0.1): | |
| result.successful = False | |
| result.reasoning = ( | |
| f"Edit validation failed: output was only {post_word_count} words " | |
| f"(original was {pre_word_count}). Original preserved." | |
| ) | |
| st.session_state.workspace.update_document( | |
| st.session_state.workspace.version_history[-1]["content"], | |
| "Rollback: edit validation failed" | |
| ) | |
| return result | |
| # Compute file cross-references | |
| doc_content = st.session_state.workspace.current_document["content"] | |
| file_acc = st.session_state.intelligence_core["file_accumulator"] | |
| cross_refs = file_acc.get_cross_references(doc_content) | |
| st.session_state.pending_file_suggestions = cross_refs | |
| return result | |
| else: | |
| return f"⚠️ {result.reasoning}" | |
| # Analysis command | |
| if "analyze document" in q or "profile document" in q: | |
| with st.spinner("Analyzing document..."): | |
| profile = profile_current_document() | |
| st.session_state.doc_profile_cache = profile | |
| result = f"## Document Analysis\n\n" | |
| result += f"**Tone:** {profile.content.get('tone', 'unknown').title()}\n" | |
| result += f"**Purpose:** {profile.content.get('primary_purpose', 'unknown').title()}\n" | |
| result += f"**Reading Level:** {profile.content.get('reading_level', 'unknown')}\n\n" | |
| if profile.strengths: | |
| result += "**Strengths:**\n" | |
| for s in profile.strengths[:3]: | |
| result += f"- {s}\n" | |
| result += "\n" | |
| if profile.suggestions: | |
| result += "**Suggestions:**\n" | |
| for s in profile.suggestions[:3]: | |
| result += f"- {s}\n" | |
| return result | |
| # Clear context | |
| if any(phrase in q for phrase in ["clear context", "new chat", "start fresh"]): | |
| st.session_state.workspace = DocumentWorkspace() | |
| st.session_state.intelligence_core["conversation_manager"].clear() | |
| st.session_state.intelligence_core["file_accumulator"].clear() | |
| st.session_state.chat_history = [] | |
| st.session_state.uploaded_files = {} | |
| st.session_state.pending_clarification = None | |
| st.session_state.pending_file_suggestions = [] | |
| return "✨ Everything cleared! Ready for a new session." | |
| # What is a word? | |
| if q == "what is a word": | |
| return "A **Word document** (.docx) is created by Microsoft Word. Try 'make a word about dogs'" | |
| # Excel generation | |
| if "make an excel" in q or "create an excel" in q or "generate an excel" in q: | |
| topic = q.replace("make an excel", "").replace("create an excel", "").replace("generate an excel", "").strip() | |
| topic = topic or "Sample_Data" | |
| data_rows = [ | |
| ["Item", "Category", "Quantity", "Price", "Total"], | |
| ["Product A", "Electronics", 10, 99.99, 999.90], | |
| ["Product B", "Clothing", 25, 49.99, 1249.75], | |
| ["Product C", "Food", 50, 9.99, 499.50] | |
| ] | |
| excel_data = create_real_excel_file(topic, data_rows) | |
| if excel_data: | |
| st.session_state.excel_data = excel_data | |
| st.session_state.excel_topic = topic | |
| st.session_state.show_excel_download = True | |
| return f"📊 Created Excel file: {topic}. Scroll down to download!" | |
| # PowerPoint generation | |
| if any(phrase in q for phrase in ["make a ppt", "create a powerpoint"]): | |
| topic = q.replace("make a ppt", "").replace("create a powerpoint", "").strip() or "Presentation" | |
| content = f"Introduction to {topic}\n- Key point 1\n- Key point 2\n\nConclusion\n- Summary" | |
| ppt_bytes = create_ppt_from_content(topic, content) | |
| if ppt_bytes: | |
| st.session_state.ppt_data = ppt_bytes | |
| st.session_state.ppt_topic = topic | |
| st.session_state.show_ppt_download = True | |
| return f"📊 Created PowerPoint: {topic}. Scroll down to download!" | |
| # Word generation | |
| if any(phrase in q for phrase in ["make a word", "create a document"]): | |
| topic = q.replace("make a word", "").replace("create a document", "").strip() or "Document" | |
| content = f"# {topic}\n\nThis document covers important information about {topic}.\n\n## Introduction\n\nContent here.\n\n## Conclusion\n\nSummary." | |
| word_bytes = create_word_from_content(topic, content) | |
| if word_bytes: | |
| st.session_state.word_data = word_bytes | |
| st.session_state.word_topic = topic | |
| st.session_state.show_word_download = True | |
| return f"📄 Created Word document: {topic}. Scroll down to download!" | |
| # Image generation | |
| if any(phrase in q for phrase in ["generate image", "create image"]): | |
| image_prompt = q.replace("generate image", "").replace("create image", "").strip() | |
| if not image_prompt: | |
| image_prompt = "a beautiful landscape" | |
| return generate_and_display_image(image_prompt) | |
| # Item 9: Streaming for non-edit queries — use stream_callback if provided | |
| search_result = internet_search(query) | |
| context = get_current_datetime() | |
| if search_result: | |
| context += "\n" + search_result | |
| if stream_callback: | |
| messages = [ | |
| {"role": "system", "content": "You are MozeAI, a helpful AI assistant."}, | |
| {"role": "user", "content": f"{context}\n\nUSER QUESTION: {query}\n\nANSWER:"} | |
| ] | |
| handler = st.session_state.intelligence_core["streaming_handler"] | |
| return handler.stream_completion(messages, on_token=stream_callback) | |
| else: | |
| answer = reason(query, context) | |
| return answer | |
| def handle_document_command(command: str) -> str: | |
| """Handle slash commands""" | |
| cmd = command.lower().strip() | |
| workspace = st.session_state.workspace | |
| core = st.session_state.intelligence_core | |
| if cmd == "/analyze": | |
| analysis = workspace.analyze_document() | |
| return f"""## Document Analysis | |
| **Structure:** {len(analysis['structure']['headings'])} headings, {analysis['structure']['paragraph_count']} paragraphs | |
| **Readability:** {analysis['readability']['level']} | |
| **Style:** {analysis['style_analysis']['detected_style']} | |
| **Suggestions:** | |
| {chr(10).join(f'- {s}' for s in analysis['suggestions'])}""" | |
| elif cmd == "/stats": | |
| meta = workspace.current_document["metadata"] | |
| return f"""## Document Stats | |
| **Title:** {workspace.current_document['title']} | |
| **Words:** {meta['word_count']} | |
| **Characters:** {meta['char_count']} | |
| **Reading Time:** {meta['reading_time']} min | |
| **Versions:** {len(workspace.version_history)}""" | |
| elif cmd.startswith("/version"): | |
| parts = cmd.split() | |
| if len(parts) > 1 and parts[1].isdigit(): | |
| if workspace.restore_version(int(parts[1])): | |
| return f"✅ Restored version {parts[1]}" | |
| return f"Versions: {len(workspace.version_history)} saved" | |
| elif cmd == "/conversation": | |
| summary = core["conversation_manager"].summarize_intent(client) | |
| return f"**Conversation Intent:** {summary}\n**Turns:** {len(core['conversation_manager'].turns)}" | |
| elif cmd == "/help": | |
| return """## Commands | |
| **Document:** `/analyze`, `/stats`, `/version N` | |
| **Conversation:** `/conversation`, `/clear` | |
| **Editing:** Just tell me what to do, like "make this formal" or "add a conclusion" """ | |
| else: | |
| return f"Unknown command. Type `/help` for available commands." | |
| # ============================================================================ | |
| # FIX #2: CLARIFICATION DIALOG COMPONENT | |
| # ============================================================================ | |
| def render_clarification_dialog(): | |
| """FIX #2: Show clarification questions when instruction is ambiguous""" | |
| pending = st.session_state.get("pending_clarification") | |
| if not pending: | |
| return | |
| st.warning("🤔 I need a bit more info to edit your document precisely:") | |
| with st.container(): | |
| st.markdown(f"**Your instruction:** _{pending['instruction']}_") | |
| answers = {} | |
| for i, question in enumerate(pending["questions"]): | |
| answer = st.text_input(f"Q{i+1}: {question}", key=f"clarif_q_{i}") | |
| answers[question] = answer | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("✅ Proceed with clarification", use_container_width=True, type="primary"): | |
| # Build enriched instruction | |
| clarifications = "; ".join([f"{q}: {a}" for q, a in answers.items() if a]) | |
| enriched = f"{pending['instruction']}. Clarifications: {clarifications}" | |
| st.session_state.pending_clarification = None | |
| # Now execute with enriched instruction | |
| with st.spinner("Applying edit..."): | |
| result = handle_intelligent_edit(enriched) | |
| if result and result.successful: | |
| st.success("✅ Done!") | |
| st.rerun() | |
| with col2: | |
| if st.button("⏭️ Skip & proceed anyway", use_container_width=True): | |
| st.session_state.pending_clarification = None | |
| with st.spinner("Applying edit..."): | |
| result = handle_intelligent_edit(pending["instruction"]) | |
| if result and result.successful: | |
| st.rerun() | |
| # ============================================================================ | |
| # FIX #4: FILE SUGGESTIONS BANNER | |
| # ============================================================================ | |
| def render_file_suggestions(): | |
| """FIX #4: Show cross-reference suggestions from uploaded files""" | |
| suggestions = st.session_state.get("pending_file_suggestions", []) | |
| if not suggestions: | |
| return | |
| with st.expander("💡 File Reference Opportunities", expanded=True): | |
| for suggestion in suggestions[:3]: | |
| st.info( | |
| f"📎 **{suggestion['file']}** — " | |
| f"matches terms in your document: `{'`, `'.join(suggestion['matched_terms'][:3])}`\n\n" | |
| f"{suggestion['suggestion']}" | |
| ) | |
| if st.button("✖ Dismiss", key="dismiss_file_suggestions"): | |
| st.session_state.pending_file_suggestions = [] | |
| st.rerun() | |
| # ============================================================================ | |
| # ITEM 4: SMART ROLLBACK / UNDO BY INTENT | |
| # ============================================================================ | |
| def smart_rollback(target_description: str = "") -> bool: | |
| """ | |
| Item 4: Rollback to the best matching version by intent keyword. | |
| If no keyword given, rolls back one version. | |
| """ | |
| workspace = st.session_state.workspace | |
| versions = workspace.version_history | |
| if not versions: | |
| return False | |
| if not target_description: | |
| # Simple one-step undo: restore second-to-last "After:" version | |
| after_versions = [v for v in versions if v["description"].startswith("After:")] | |
| if len(after_versions) >= 2: | |
| workspace.restore_version(after_versions[-2]["id"]) | |
| return True | |
| return False | |
| # Keyword search across version descriptions | |
| keyword = target_description.lower() | |
| best = None | |
| for v in reversed(versions): | |
| if keyword in v["description"].lower(): | |
| best = v | |
| break | |
| if best: | |
| workspace.restore_version(best["id"]) | |
| return True | |
| return False | |
| # ============================================================================ | |
| # ITEM 5: CONVERSATION DASHBOARD | |
| # ============================================================================ | |
| def render_conversation_dashboard(): | |
| """Item 5: Expandable timeline of turns + word-count evolution""" | |
| core = st.session_state.get("intelligence_core") | |
| if not core: | |
| return | |
| conv = core["conversation_manager"] | |
| if not conv.turns: | |
| st.caption("No conversation yet — start editing to see the timeline.") | |
| return | |
| evolution = conv.get_document_evolution() | |
| total_edits = conv.cumulative_edits["total_edits"] | |
| intent = conv.intent_summary or "Not summarized yet" | |
| st.markdown(f"**Session intent:** _{intent}_") | |
| st.caption(f"Total edits: {total_edits} | Turns: {len(evolution)}") | |
| for i, turn in enumerate(evolution, 1): | |
| changes = turn.get("changes", {}) | |
| net = changes.get("net_change", 0) | |
| old_wc = turn["document_state"].get("word_count", 0) | |
| new_wc = changes.get("new_word_count", old_wc + net) | |
| arrow = "📈" if net > 0 else ("📉" if net < 0 else "➡️") | |
| label = turn["query"][:45] + ("…" if len(turn["query"]) > 45 else "") | |
| st.markdown( | |
| f"**{i}.** {arrow} _{label}_ \n" | |
| f"<span style='color:#888;font-size:12px'>{old_wc} → {new_wc} words ({net:+d})</span>", | |
| unsafe_allow_html=True | |
| ) | |
| # Smart rollback controls | |
| st.markdown("---") | |
| st.markdown("**↩ Undo / Rollback**") | |
| col_a, col_b = st.columns([2, 1]) | |
| with col_a: | |
| rollback_kw = st.text_input("Roll back to edit containing…", placeholder="e.g. 'formal'", | |
| key="rollback_kw", label_visibility="collapsed") | |
| with col_b: | |
| if st.button("↩ Undo", use_container_width=True): | |
| keyword = rollback_kw.strip() if rollback_kw.strip() else "" | |
| if smart_rollback(keyword): | |
| st.success("✅ Rolled back!") | |
| st.rerun() | |
| else: | |
| st.warning("No matching version found.") | |
| # ============================================================================ | |
| # ITEM 7: EDIT PLAN DISPLAY | |
| # ============================================================================ | |
| def render_edit_plan(plan) -> None: | |
| """Item 7: Show the AI's edit plan as an expandable checklist in chat""" | |
| if not plan: | |
| return | |
| with st.expander("🗺️ Edit Plan", expanded=False): | |
| st.markdown(f"**Strategy:** {plan.strategy}") | |
| for step in plan.steps: | |
| st.markdown(f"- ☑ {step}") | |
| if plan.constraints: | |
| st.markdown("**Constraints:** " + " · ".join(plan.constraints)) | |
| if plan.rationale: | |
| st.caption(f"Rationale: {plan.rationale}") | |
| # ============================================================================ | |
| # ITEM 8: CUMULATIVE TIMELINE IN SIDEBAR | |
| # ============================================================================ | |
| def render_cumulative_timeline(): | |
| """Item 8: Compact edit timeline for sidebar display""" | |
| core = st.session_state.get("intelligence_core") | |
| if not core: | |
| return | |
| conv = core["conversation_manager"] | |
| if not conv.turns: | |
| return | |
| st.markdown("**📅 Edit Timeline**") | |
| for i, turn in enumerate(conv.turns[-5:], 1): | |
| changes = turn.edits_made | |
| net = changes.get("net_change", 0) | |
| label = turn.user_query[:30] + ("…" if len(turn.user_query) > 30 else "") | |
| color = "#4CAF50" if net >= 0 else "#f44336" | |
| st.markdown( | |
| f"<small>{i}. {label} " | |
| f"<span style='color:{color};font-weight:bold'>{net:+d}w</span></small>", | |
| unsafe_allow_html=True | |
| ) | |
| # ============================================================================ | |
| # UI COMPONENTS | |
| # ============================================================================ | |
| def render_document_explorer(): | |
| with st.sidebar: | |
| st.markdown("### 📁 Document Explorer") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("📄 New", use_container_width=True): | |
| st.session_state.workspace.current_document["content"] = "" | |
| st.session_state.workspace.current_document["title"] = "Untitled Document" | |
| st.session_state.workspace.save_version("New document") | |
| st.rerun() | |
| with col2: | |
| if st.button("💾 Save", use_container_width=True): | |
| st.session_state.workspace.save_version("Manual save") | |
| st.success("Saved!") | |
| st.markdown("---") | |
| uploaded_files = st.file_uploader( | |
| "Upload files", | |
| type=['pdf', 'docx', 'txt', 'csv', 'json'], | |
| accept_multiple_files=True, | |
| key="file_uploader" | |
| ) | |
| if uploaded_files: | |
| for file in uploaded_files: | |
| if file.name not in st.session_state.uploaded_files: | |
| content = process_uploaded_file(file) | |
| if content and not content.startswith("Error"): | |
| st.session_state.uploaded_files[file.name] = content | |
| st.session_state.intelligence_core["file_accumulator"].add_file( | |
| file.name, file.type, content, {} | |
| ) | |
| st.success(f"✅ {file.name}") | |
| # Item 3: Multi-file selection — let user choose which files to reference in next edit | |
| all_files = list(st.session_state.uploaded_files.keys()) | |
| if all_files: | |
| st.markdown("**📎 Reference in next edit**") | |
| selected = st.multiselect( | |
| "Select files to inject", | |
| options=all_files, | |
| default=[], | |
| key="selected_ref_files", | |
| label_visibility="collapsed" | |
| ) | |
| st.session_state.selected_ref_files = selected | |
| if selected: | |
| st.caption(f"✅ {len(selected)} file(s) will be injected into the edit prompt") | |
| st.markdown("---") | |
| st.markdown("**Version History**") | |
| if st.button("📜 View Versions", use_container_width=True): | |
| versions = st.session_state.workspace.version_history | |
| if versions: | |
| for v in versions[-3:]: | |
| st.caption(f"v{v['id']}: {v['description'][:30]}") | |
| def render_document_editor(): | |
| st.markdown("### 📝 Document Editor") | |
| new_title = st.text_input( | |
| "Title", | |
| value=st.session_state.workspace.current_document["title"], | |
| key="doc_title" | |
| ) | |
| if new_title != st.session_state.workspace.current_document["title"]: | |
| st.session_state.workspace.current_document["title"] = new_title | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| if st.button("🔍 Analyze", use_container_width=True): | |
| analysis = st.session_state.workspace.analyze_document() | |
| st.session_state.last_analysis = analysis | |
| st.info(f"Readability: {analysis['readability']['level']}") | |
| with col2: | |
| track_status = "✅ Track ON" if st.session_state.workspace.track_changes else "⭕ Track OFF" | |
| if st.button(track_status, use_container_width=True): | |
| st.session_state.workspace.track_changes = not st.session_state.workspace.track_changes | |
| st.rerun() | |
| with col3: | |
| if st.button("📊 Stats", use_container_width=True): | |
| meta = st.session_state.workspace.current_document["metadata"] | |
| st.info(f"{meta['word_count']} words, {meta['reading_time']} min read") | |
| with col4: | |
| if st.button("🧹 Clear", use_container_width=True): | |
| st.session_state.workspace.current_document["content"] = "" | |
| st.rerun() | |
| st.markdown("---") | |
| content = st.text_area( | |
| "Content", | |
| value=st.session_state.workspace.current_document["content"], | |
| height=400, | |
| key="doc_editor", | |
| label_visibility="collapsed" | |
| ) | |
| if content != st.session_state.workspace.current_document["content"]: | |
| st.session_state.workspace.update_document(content, "Manual edit") | |
| def render_ai_copilot(): | |
| with st.sidebar: | |
| st.markdown("### 🤖 AI Copilot") | |
| # Intent summary | |
| core = st.session_state.get("intelligence_core") | |
| if core: | |
| conv_manager = core["conversation_manager"] | |
| if conv_manager.turns: | |
| intent = conv_manager.intent_summary or conv_manager.summarize_intent(client) | |
| if intent and "No conversation" not in intent: | |
| st.info(f"📍 **Goal:** {intent}") | |
| total = conv_manager.cumulative_edits["total_edits"] | |
| if total > 0: | |
| st.caption(f"🔄 {total} edit{'s' if total != 1 else ''} this session") | |
| st.markdown("---") | |
| quick_actions = [ | |
| ("✨ Improve", "improve this document"), | |
| ("🎓 Academic", "make this academic"), | |
| ("📝 Summarize", "summarize this document"), | |
| ("🔧 Fix Grammar", "fix grammar"), | |
| ] | |
| for label, instruction in quick_actions: | |
| if st.button(label, use_container_width=True): | |
| with st.spinner("AI editing..."): | |
| result = handle_intelligent_edit(instruction) | |
| if result and result.successful: | |
| st.success("Done!") | |
| st.rerun() | |
| st.markdown("---") | |
| custom = st.text_area("Custom instruction", placeholder="e.g., 'Rewrite for a 12-year-old'", height=80) | |
| if st.button("Apply", use_container_width=True, type="primary"): | |
| if custom: | |
| with st.spinner("AI working..."): | |
| result = handle_intelligent_edit(custom) | |
| if result and result.successful: | |
| st.success("Updated!") | |
| st.rerun() | |
| st.markdown("---") | |
| # Item 8: Cumulative timeline | |
| render_cumulative_timeline() | |
| st.markdown("---") | |
| # Item 5: Conversation dashboard in expander | |
| with st.expander("📊 Conversation Dashboard", expanded=False): | |
| render_conversation_dashboard() | |
| st.markdown("---") | |
| st.markdown("**Quick Stats**") | |
| meta = st.session_state.workspace.current_document["metadata"] | |
| st.caption(f"Words: {meta['word_count']}") | |
| st.caption(f"Versions: {len(st.session_state.workspace.version_history)}") | |
| # ============================================================================ | |
| # FULLY UPGRADED CHAT INTERFACE | |
| # Items: 1 (token metrics), 3 (selected_files), 7 (plan display), | |
| # 9 (streaming non-edit), 10 (keyboard shortcut hint) | |
| # ============================================================================ | |
| def render_chat_interface(): | |
| st.markdown("---") | |
| st.markdown("### 💬 Chat") | |
| # Show clarification dialog if pending | |
| render_clarification_dialog() | |
| # Show file suggestions if any | |
| render_file_suggestions() | |
| # Show chat history | |
| for role, msg in st.session_state.chat_history[-10:]: | |
| with st.chat_message(role): | |
| st.markdown(msg) | |
| # Item 10: Keyboard shortcut hint | |
| st.caption("💡 Tip: Press **Enter** to send · Use `/help` for commands · `Ctrl+Z` style undo: type **undo**") | |
| query = st.chat_input("Ask me to edit, analyze, or generate…") | |
| # Item 10: "undo" as a text shortcut for smart rollback | |
| if query and query.strip().lower() in ("undo", "undo last"): | |
| with st.chat_message("user"): | |
| st.markdown(query) | |
| st.session_state.chat_history.append(("user", query)) | |
| with st.chat_message("assistant"): | |
| if smart_rollback(): | |
| msg = "↩️ Undone — restored the previous version." | |
| else: | |
| msg = "⚠️ Nothing to undo." | |
| st.markdown(msg) | |
| st.session_state.chat_history.append(("assistant", msg)) | |
| st.rerun() | |
| return | |
| if query: | |
| st.session_state.chat_history.append(("user", query)) | |
| with st.chat_message("user"): | |
| st.markdown(query) | |
| with st.chat_message("assistant"): | |
| q_lower = query.lower().strip() | |
| edit_keywords = ["improve", "rewrite", "summarize", "expand", "shorten", | |
| "fix grammar", "make formal", "make academic", "translate"] | |
| is_edit = (any(kw in q_lower for kw in edit_keywords) | |
| and st.session_state.workspace.current_document["content"] | |
| and not q_lower.startswith("/")) | |
| if is_edit: | |
| # Item 7: Show plan before streaming starts | |
| plan_placeholder = st.empty() | |
| # Item 1: Prepare live token-speed display | |
| response_placeholder = st.empty() | |
| streaming_text = "" | |
| metrics_placeholder = st.empty() | |
| stream_start = time.time() | |
| token_count_ref = [0] | |
| def display_token(token: str): | |
| nonlocal streaming_text | |
| streaming_text += token | |
| token_count_ref[0] += 1 | |
| elapsed = max(time.time() - stream_start, 0.001) | |
| tps = round(token_count_ref[0] / elapsed, 1) | |
| wc = len(streaming_text.split()) | |
| response_placeholder.markdown(streaming_text + "▌") | |
| # Item 1: live token speed | |
| metrics_placeholder.caption( | |
| f"✍️ {wc} words · {token_count_ref[0]} tokens · **{tps} tok/s**" | |
| ) | |
| # Item 3: pick up selected files from sidebar | |
| selected_files = st.session_state.get("selected_ref_files", []) or None | |
| with st.spinner(""): | |
| response = run_agent(query, | |
| stream_callback=display_token, | |
| selected_files=selected_files) | |
| metrics_placeholder.empty() | |
| if response == "__CLARIFICATION_NEEDED__": | |
| response_placeholder.empty() | |
| plan_placeholder.empty() | |
| st.rerun() | |
| return | |
| elif isinstance(response, EditResult) and response.successful: | |
| response_placeholder.empty() | |
| plan_placeholder.empty() | |
| # Item 7: Retrieve and display the edit plan used | |
| core = st.session_state.intelligence_core | |
| last_plan = None | |
| try: | |
| doc_snap = { | |
| "content": st.session_state.workspace.current_document["content"], | |
| "title": st.session_state.workspace.current_document["title"], | |
| "word_count": len(st.session_state.workspace.current_document["content"].split()) | |
| } | |
| parsed = core["instruction_parser"].parse(query, doc_snap) | |
| last_plan = core["edit_planner"].plan(parsed, doc_snap, core["conversation_manager"]) | |
| except Exception: | |
| pass | |
| if last_plan: | |
| render_edit_plan(last_plan) | |
| # Metrics card | |
| changes = response.changes_made | |
| old_wc = changes.get("old_word_count", 0) | |
| new_wc = changes.get("new_word_count", 0) | |
| net = changes.get("net_change", 0) | |
| additions = changes.get("additions", 0) | |
| deletions = changes.get("deletions", 0) | |
| exec_ms = response.execution_time_ms | |
| # Item 1: Pull token speed from streaming handler | |
| sh = core["streaming_handler"] | |
| tps = sh.last_metrics.get("tokens_per_sec", 0) | |
| ttft = sh.last_metrics.get("ttft_ms", 0) | |
| token_total = sh.last_metrics.get("token_count", 0) | |
| st.success("✅ Edit Complete") | |
| col1, col2, col3, col4, col5 = st.columns(5) | |
| with col1: | |
| st.metric("Words Added", f"+{additions}" if additions else "0") | |
| with col2: | |
| st.metric("Words Removed", f"-{deletions}" if deletions else "0") | |
| with col3: | |
| st.metric("Net Change", f"{net:+d}") | |
| with col4: | |
| st.metric("Time", f"{exec_ms}ms") | |
| with col5: | |
| # Item 1: token speed metric | |
| st.metric("Speed", f"{tps} tok/s") | |
| st.caption( | |
| f"📝 {old_wc} → {new_wc} words · " | |
| f"{token_total} tokens · TTFT {ttft}ms" | |
| ) | |
| if response.reasoning: | |
| st.info(f"**Why:** {response.reasoning}") | |
| summary_msg = ( | |
| f"✅ **Edit complete** in {exec_ms}ms · {tps} tok/s\n\n" | |
| f"Words: {old_wc} → {new_wc} ({net:+d})\n\n" | |
| f"Reasoning: {response.reasoning}" | |
| ) | |
| st.session_state.chat_history.append(("assistant", summary_msg)) | |
| elif isinstance(response, EditResult) and not response.successful: | |
| response_placeholder.warning(f"⚠️ {response.reasoning}") | |
| st.session_state.chat_history.append(("assistant", f"⚠️ {response.reasoning}")) | |
| elif isinstance(response, str): | |
| response_placeholder.markdown(response) | |
| st.session_state.chat_history.append(("assistant", response)) | |
| else: | |
| # Item 9: Streaming for non-edit queries | |
| response_placeholder = st.empty() | |
| metrics_placeholder = st.empty() | |
| streaming_text = "" | |
| stream_start = time.time() | |
| token_count_ref = [0] | |
| def display_token_general(token: str): | |
| nonlocal streaming_text | |
| streaming_text += token | |
| token_count_ref[0] += 1 | |
| elapsed = max(time.time() - stream_start, 0.001) | |
| tps = round(token_count_ref[0] / elapsed, 1) | |
| response_placeholder.markdown(streaming_text + "▌") | |
| metrics_placeholder.caption(f"⚡ {tps} tok/s") | |
| with st.spinner(""): | |
| response = run_agent(query, stream_callback=display_token_general) | |
| metrics_placeholder.empty() | |
| response_placeholder.empty() | |
| if isinstance(response, str): | |
| st.markdown(response) | |
| st.session_state.chat_history.append(("assistant", response)) | |
| st.rerun() | |
| def render_download_buttons(): | |
| if st.session_state.get("show_ppt_download", False) and st.session_state.get("ppt_data"): | |
| st.download_button( | |
| label="📥 Download PowerPoint", | |
| data=st.session_state.ppt_data, | |
| file_name=f"{st.session_state.ppt_topic}.pptx", | |
| mime="application/vnd.openxmlformats-officedocument.presentationml.presentation" | |
| ) | |
| st.session_state.show_ppt_download = False | |
| if st.session_state.get("show_word_download", False) and st.session_state.get("word_data"): | |
| st.download_button( | |
| label="📥 Download Word Document", | |
| data=st.session_state.word_data, | |
| file_name=f"{st.session_state.word_topic}.docx", | |
| mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document" | |
| ) | |
| st.session_state.show_word_download = False | |
| if st.session_state.get("show_excel_download", False) and st.session_state.get("excel_data"): | |
| st.download_button( | |
| label="📥 Download Excel File", | |
| data=st.session_state.excel_data, | |
| file_name=f"{st.session_state.excel_topic}.xlsx", | |
| mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" | |
| ) | |
| st.session_state.show_excel_download = False | |
| # ============================================================================ | |
| # MAIN APPLICATION | |
| # ============================================================================ | |
| def init_session_state(): | |
| """Initialize all session state variables""" | |
| if "workspace" not in st.session_state: | |
| st.session_state.workspace = DocumentWorkspace() | |
| if "chat_history" not in st.session_state: | |
| st.session_state.chat_history = [] | |
| if "uploaded_files" not in st.session_state: | |
| st.session_state.uploaded_files = {} | |
| if "show_ppt_download" not in st.session_state: | |
| st.session_state.show_ppt_download = False | |
| st.session_state.ppt_data = None | |
| st.session_state.ppt_topic = "" | |
| if "show_word_download" not in st.session_state: | |
| st.session_state.show_word_download = False | |
| st.session_state.word_data = None | |
| st.session_state.word_topic = "" | |
| if "show_excel_download" not in st.session_state: | |
| st.session_state.show_excel_download = False | |
| st.session_state.excel_data = None | |
| st.session_state.excel_topic = "" | |
| if "intelligence_core" not in st.session_state: | |
| st.session_state.intelligence_core = None | |
| if "doc_profile_cache" not in st.session_state: | |
| st.session_state.doc_profile_cache = None | |
| if "last_model_used" not in st.session_state: | |
| st.session_state.last_model_used = None | |
| if "last_analysis" not in st.session_state: | |
| st.session_state.last_analysis = None | |
| # FIX #2: Clarification state | |
| if "pending_clarification" not in st.session_state: | |
| st.session_state.pending_clarification = None | |
| # FIX #4: File suggestions state | |
| if "pending_file_suggestions" not in st.session_state: | |
| st.session_state.pending_file_suggestions = [] | |
| # Item 3: Multi-file selection state | |
| if "selected_ref_files" not in st.session_state: | |
| st.session_state.selected_ref_files = [] | |
| def apply_custom_css(): | |
| st.markdown(""" | |
| <style> | |
| .stMain { | |
| background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); | |
| } | |
| h1 { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .stButton button { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| border-radius: 8px; | |
| } | |
| .stTextArea textarea { | |
| border-radius: 12px; | |
| font-family: monospace; | |
| } | |
| /* Metric cards */ | |
| [data-testid="stMetric"] { | |
| background: rgba(102, 126, 234, 0.08); | |
| border-radius: 10px; | |
| padding: 8px; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| def main(): | |
| st.set_page_config( | |
| page_title="MozeAI Document Studio", | |
| page_icon="📝", | |
| layout="wide" | |
| ) | |
| init_session_state() | |
| apply_custom_css() | |
| # Initialize Groq client | |
| groq_api_key = None | |
| try: | |
| if "GROQ_API_KEY" in st.secrets: | |
| groq_api_key = st.secrets["GROQ_API_KEY"] | |
| except: | |
| pass | |
| if not groq_api_key: | |
| groq_api_key = os.environ.get("GROQ_API_KEY") | |
| if not groq_api_key: | |
| st.error("GROQ_API_KEY not found. Please set it in secrets or environment.") | |
| st.stop() | |
| global client | |
| client = Groq(api_key=groq_api_key) | |
| # Initialize intelligence core if not exists | |
| if st.session_state.intelligence_core is None: | |
| streaming_handler = StreamingResponseHandler(client) | |
| contextual_editor = ContextualEditor(client, streaming_handler) | |
| st.session_state.intelligence_core = { | |
| "conversation_manager": ConversationManager(), | |
| "instruction_parser": InstructionParser(client), | |
| "file_accumulator": FileContextAccumulator(client), | |
| "edit_planner": EditPlanner(client), | |
| "streaming_handler": streaming_handler, | |
| "document_profiler": DocumentProfiler(client), | |
| "contextual_editor": contextual_editor | |
| } | |
| # Header | |
| st.markdown('<h1 style="text-align: center;">📝 MozeAI Document Studio</h1>', unsafe_allow_html=True) | |
| st.markdown('<p style="text-align: center; color: #667eea;">Intelligent Document Workspace</p>', unsafe_allow_html=True) | |
| st.markdown("---") | |
| # Sidebar with tabs | |
| with st.sidebar: | |
| tab1, tab2 = st.tabs(["📁 Explorer", "🤖 Copilot"]) | |
| with tab1: | |
| render_document_explorer() | |
| with tab2: | |
| render_ai_copilot() | |
| # Main content | |
| render_document_editor() | |
| # Download buttons | |
| render_download_buttons() | |
| # Chat interface | |
| render_chat_interface() | |
| # Footer | |
| st.markdown("---") | |
| st.markdown( | |
| '<p style="text-align: center; color: #888; font-size: 12px;">MozeAI Document Studio | Created by Mukiibi Moses</p>', | |
| unsafe_allow_html=True | |
| ) | |
| if __name__ == "__main__": | |
| main() |