diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,3 +1,8 @@ +""" +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 @@ -6,7 +11,7 @@ import numpy as np from bs4 import BeautifulSoup import PyPDF2 import docx -from io import StringIO +from io import StringIO, BytesIO import csv import json from datetime import datetime @@ -15,10 +20,14 @@ 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 FUNCTIONS -# ============================================ +# ============================================================================ +# DOCUMENT GENERATION LIBRARIES +# ============================================================================ from pptx import Presentation from pptx.util import Inches, Pt @@ -27,1469 +36,2623 @@ 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 -import io -import base64 -def create_ppt_from_content(title, content, filename="presentation"): - """Create a PowerPoint presentation from content - properly split across slides""" - try: - prs = Presentation() +# ============================================================================ +# DATA CLASSES FOR INTELLIGENCE CORE +# ============================================================================ + +@dataclass +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) + + +@dataclass +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 + + +@dataclass +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) + + +@dataclass +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 + + +@dataclass +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 - # Title slide - 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')}" + def update_document(self, new_content, change_description=""): + """Update document with change tracking""" + old_content = self.current_document["content"] - # Content slides layout - content_slide_layout = prs.slide_layouts[1] + if old_content == new_content: + return False + + if self.track_changes: + self.save_version(f"Before: {change_description}") - # Split content into slides based on headings or paragraphs - lines = content.split('\n') + changes = self._calculate_changes(old_content, new_content) - current_slide = None - current_text_frame = None - current_title = None + self.current_document["content"] = new_content + self.current_document["modified_at"] = datetime.now().isoformat() - for line in lines: - line = line.strip() - if not line: - continue - - # Check if this line looks like a slide title - is_title = False - if len(line) < 60 and (line.endswith(':') or line.isupper() or re.match(r'^\d+\.', line) or line[0].isupper() and len(line) < 40): - is_title = True + 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}") - if is_title: - # Create new slide for this title - current_slide = prs.slides.add_slide(content_slide_layout) - clean_title = line.rstrip(':') - current_slide.shapes.title.text = clean_title[:100] - content_box = current_slide.placeholders[1] - current_text_frame = content_box.text_frame - current_text_frame.text = "" - current_title = clean_title - else: - # If no slide exists yet, create one - if current_slide is None: - current_slide = prs.slides.add_slide(content_slide_layout) - current_slide.shapes.title.text = "Information" - content_box = current_slide.placeholders[1] - current_text_frame = content_box.text_frame - current_text_frame.text = "" - - # Add as bullet point - if current_text_frame: - p = current_text_frame.add_paragraph() - p.text = line[:150] - p.font.size = Pt(18) - p.level = 0 - p.space_after = Pt(6) - - # If no content slides were created, add a default one - if len(prs.slides) == 1: - slide = prs.slides.add_slide(content_slide_layout) - slide.shapes.title.text = "Content Summary" - content_box = slide.placeholders[1] - text_frame = content_box.text_frame - text_frame.text = content[:500] - - ppt_bytes = io.BytesIO() - prs.save(ppt_bytes) - ppt_bytes.seek(0) - return ppt_bytes - except Exception as e: - print(f"PPT creation error: {e}") - return None - -def create_word_from_content(title, content, filename="document"): - """Create a Word document from content""" - try: - doc = WordDocument() + 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') - title_heading = doc.add_heading(title, 0) - title_heading.alignment = WD_ALIGN_PARAGRAPH.CENTER + diff = list(unified_diff(old_lines, new_lines, lineterm='')) - doc.add_paragraph(f"Generated by MozeAI on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - doc.add_paragraph() + 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) - paragraphs = content.split('\n\n') - for para in paragraphs: - if para.strip(): - p = doc.add_paragraph(para.strip()) - p.style.font.size = DocPt(12) + self.current_document["metadata"]["word_count"] = words + self.current_document["metadata"]["char_count"] = chars + self.current_document["metadata"]["reading_time"] = max(1, words // 200) - word_bytes = io.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): - """Create a REAL .xlsx Excel file with proper formatting""" - try: - from openpyxl import Workbook - from openpyxl.styles import Font, PatternFill, Alignment - from openpyxl.utils import get_column_letter - from io import BytesIO + 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) - wb = Workbook() - ws = wb.active - ws.title = title[:31].replace('/', '_').replace('\\', '_') + 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"] - # Write data to worksheet - 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) - - # Style header row - if row_idx == 1: - cell.font = Font(bold=True, color="FFFFFF") - cell.fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") - cell.alignment = Alignment(horizontal="center", vertical="center") - else: - cell.alignment = Alignment(horizontal="left", vertical="center") + analysis = { + "structure": self._analyze_structure(), + "readability": self._analyze_readability(), + "grammar_issues": self._check_grammar(), + "style_analysis": self._analyze_style(), + "suggestions": self._generate_suggestions() + } - # Auto-adjust column widths - 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 - adjusted_width = min(max_length + 2, 50) - ws.column_dimensions[get_column_letter(col[0].column)].width = adjusted_width + return analysis + + def _analyze_structure(self): + """Analyze document structure""" + content = self.current_document["content"] + lines = content.split('\n') - # Save to bytes - output = BytesIO() - wb.save(output) - output.seek(0) - return output + headings = [] + paragraphs = 0 + lists = 0 - except Exception as e: - print(f"Excel creation error: {e}") - return None - -def create_csv_from_data(title, data_rows): - """Create a CSV file from data rows - Fallback""" - try: - from io import BytesIO - import csv + 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() - output = BytesIO() - output.write('\ufeff'.encode('utf-8')) + if len(sentences) == 0 or len(words) == 0: + return {"score": 0, "level": "Unknown"} - writer = csv.writer(output) - for row in data_rows: - writer.writerow(row) + avg_words_per_sentence = len(words) / len(sentences) - output.seek(0) - return output - except Exception as e: - print(f"CSV creation error: {e}") - return None - -def export_chat_history(): - """Export the entire chat history as a readable .txt file""" - if not st.session_state.chat_history: - return None - - # Create a clean, readable text format - 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" - - for idx, (role, msg) in enumerate(st.session_state.chat_history, 1): - if role == "user": - export_content += f"[{idx}] USER:\n" - export_content += "-" * 40 + "\n" - export_content += f"{msg}\n\n" + 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: - export_content += f"[{idx}] MOZEAI:\n" - export_content += "-" * 40 + "\n" - export_content += f"{msg}\n\n" + score = 10 + level = "Very Difficult" + + return {"score": score, "level": level} - export_content += "=" * 70 + "\n" - export_content += "END OF CHAT HISTORY\n" - export_content += f"Total messages: {len(st.session_state.chat_history)}\n" - export_content += "=" * 70 + 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] - return export_content - -# ============================================ -# 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" - for table in doc.tables: - for row in table.rows: - row_text = [] - for cell in row.cells: - if cell.text and cell.text.strip(): - row_text.append(cell.text.strip()) - if row_text: - text += " | ".join(row_text) + "\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" - if len(rows) > 11: - text += f"\n... and {len(rows) - 11} more rows" - return text[:5000] if text.strip() else "CSV file appears empty" - except Exception as e: - return f"Error reading CSV: {str(e)}" + 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 -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) - if len(formatted) > 3000: - text = "JSON Data Summary:\n\n" - text += f"Type: {type(data).__name__}\n" - if isinstance(data, dict): - text += f"Keys: {', '.join(list(data.keys())[:10])}\n" - elif isinstance(data, list): - text += f"Length: {len(data)}\n" - text += "\nFull JSON (truncated):\n" + formatted[:3000] - else: - text = formatted - return text[:5000] - 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}" +# ============================================================================ +# 1. CONVERSATION MANAGER +# ============================================================================ -# ============================================ -# CONFIG -# ============================================ - -TEMPERATURE = 0 -MAX_TOKENS = 800 - -st.set_page_config(page_title="MozeAI", page_icon="π§ ", layout="wide") - -# ============================================ -# CSS - FIXED CHAT INPUT AT BOTTOM -# ============================================ - -st.markdown(""" - -""", unsafe_allow_html=True) + 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? -# ============================================ -# GROQ CLIENT - FIXED FOR HUGGING FACE SPACES -# ============================================ +Queries: +{chr(10).join(f'- {q}' for q in queries)} -# Try to get API key from multiple sources -groq_api_key = None +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 + } -# Try Streamlit secrets first -try: - if "GROQ_API_KEY" in st.secrets: - groq_api_key = st.secrets["GROQ_API_KEY"] -except: - pass -# Try environment variable (for HF Spaces) -if not groq_api_key: - groq_api_key = os.environ.get("GROQ_API_KEY") +# ============================================================================ +# 2. INSTRUCTION PARSER +# ============================================================================ -# If still no key, show helpful error -if not groq_api_key: - st.error(""" - ### GROQ_API_KEY Missing +class InstructionParser: + """Extract semantic meaning from user instructions using AI""" - Please set your Groq API key to use this app. + def __init__(self, llm_client=None): + self.llm_client = llm_client + self.confidence_threshold = 0.7 - **For Hugging Face Spaces:** - 1. Go to Settings β Repository Secrets - 2. Add `GROQ_API_KEY` = `your_key_here` - 3. Restart the Space - - **For local development:** - Create `.streamlit/secrets.toml` with: - GROQ_API_KEY = "your_key_here" - """) - st.stop() - -# Initialize client -client = Groq(api_key=groq_api_key) - -def get_current_datetime(): - tz = pytz.timezone('Asia/Seoul') - now = datetime.now(tz) - return f"""Current Information: -- Date: {now.strftime('%B %d, %Y')} -- Time: {now.strftime('%I:%M %p')} -- Day: {now.strftime('%A')} -- Timezone: Asia/Seoul""" + 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}" -# ============================================ -# LIGHTWEIGHT MEMORY SYSTEM -# ============================================ +Document context: {document.get('title', 'Untitled')} ({document.get('word_count', 0)} words) -class LightweightMemory: - def __init__(self): - self.memories = [] - self.keyword_index = defaultdict(list) +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 add_memory(self, text, metadata=None): - if len(text) < 50: - return + def _parse_with_regex(self, instruction: str, document: dict) -> ParsedInstruction: + inst_lower = instruction.lower() - words = set(re.findall(r'\b[a-z]{3,}\b', text.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" - memory = { - "text": text, - "keywords": words, - "metadata": metadata or {}, - "timestamp": time.time() - } + 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" - self.memories.append(memory) + 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 - for word in words: - self.keyword_index[word].append(len(self.memories) - 1) - - if len(self.memories) > 50: - self.memories = self.memories[-50:] - self._rebuild_index() - - def _rebuild_index(self): - self.keyword_index = defaultdict(list) - for idx, memory in enumerate(self.memories): - for word in memory["keywords"]: - self.keyword_index[word].append(idx) - - def retrieve(self, query, top_k=3): - if not self.memories: - return [] - - query_words = set(re.findall(r'\b[a-z]{3,}\b', query.lower())) - - scored = [] - for idx, memory in enumerate(self.memories): - matches = len(query_words & memory["keywords"]) - if matches > 0: - scored.append((memory["text"], matches)) - - scored.sort(key=lambda x: x[1], reverse=True) - return [text for text, score in scored[:top_k]] - - def get_context(self, query): - results = self.retrieve(query) - if results: - context = "RELEVANT PAST CONVERSATIONS:\n\n" - for i, result in enumerate(results): - context += f"[{i+1}] {result}\n\n" - return context - return "" - -# Initialize memory -memory = LightweightMemory() - -def store_memory(text): - memory.add_memory(text) + 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] + ) -def retrieve_memory(query): - return memory.get_context(query) -# ============================================ -# LLM FUNCTION WITH MULTIPLE MODEL FALLBACKS -# ============================================ +# ============================================================================ +# 3. FILE CONTEXT ACCUMULATOR +# ============================================================================ -def llm_with_fallback(messages, max_retries=2): - models_to_try = [ - "llama-3.3-70b-versatile", - "llama-3.1-70b-versatile", - "mixtral-8x7b-32768", - "llama-3.1-8b-instant", - "gemma2-9b-it" - ] +class FileContextAccumulator: + """Remember all uploaded files and cross-reference them""" - for model in models_to_try: - for attempt in range(max_retries): + 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: - completion = client.chat.completions.create( - model=model, - temperature=TEMPERATURE, - max_tokens=MAX_TOKENS, - messages=messages, - timeout=30 - ) - st.session_state.last_model_used = model - return completion.choices[0].message.content.strip() + 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: - if attempt < max_retries - 1: - time.sleep(2 ** attempt) - continue + print(f"AI planning failed: {e}") + + return self._plan_with_templates(parsed, document, conversation) - return "AI service temporarily unavailable. Please try again." - -def llm(messages): - return llm_with_fallback(messages) - -# ============================================ -# SYSTEM PROMPT -# ============================================ - -SYSTEM_PROMPT = """ -You are MozeAI, an advanced AI assistant with REAL-TIME internet access, file analysis capabilities, and document generation capabilities. + 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} -================================================================================ -CREATOR INFORMATION -================================================================================ +Document: {document.get('title', 'Untitled')} ({document.get('word_count', 0)} words) -Your creator is Mukiibi Moses, a computer engineering student and AI researcher at Kyungdong University, South Korea. +{conv_context} -PORTFOLIO: https://moze12432.github.io/ +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" + ) -================================================================================ -YOUR CAPABILITIES: -================================================================================ -1. REAL-TIME web search for current information -2. File analysis for PDF, DOCX, TXT, CSV, JSON files -3. Memory of past conversations -4. Image generation and editing -5. Calculator for mathematical expressions -6. Document generation: PowerPoint, Word, Excel +# ============================================================================ +# 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)}" -================================================================================ -CRITICAL RULES: -================================================================================ -1. For questions about PEOPLE, PLACES, EVENTS, use SEARCH RESULTS -2. ONLY mention your creator when specifically asked -3. Answer concisely and accurately -4. Be conversational and friendly +# ============================================================================ +# 6. DOCUMENT PROFILER +# ============================================================================ -================================================================================ -UNDERSTANDING "EXCEL": -================================================================================ +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} -- "excel in/at life" -> VERB -> Give life advice -- "generate an excel file" -> NOUN -> Create spreadsheet +Content: +{content[:3000]} -================================================================================ -Remember: You are MozeAI - helpful, intelligent, and capable. -""" +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} + ) -# ============================================ -# SESSION STATE -# ============================================ - -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 "file_context" not in st.session_state: - st.session_state.file_context = "" -if "last_search_query" not in st.session_state: - st.session_state.last_search_query = None -if "last_search_results" not in st.session_state: - st.session_state.last_search_results = None -if "last_response" not in st.session_state: - st.session_state.last_response = None -if "last_topic" not in st.session_state: - st.session_state.last_topic = None -if "last_image_prompt" not in st.session_state: - st.session_state.last_image_prompt = None -if "code_search_cache" not in st.session_state: - st.session_state.code_search_cache = {} -if "is_resetting" not in st.session_state: - st.session_state.is_resetting = False -if "last_model_used" not in st.session_state: - st.session_state.last_model_used = None - -# Document download session states -if "show_ppt_download" not in st.session_state: - st.session_state.show_ppt_download = False -if "ppt_data" not in st.session_state: - st.session_state.ppt_data = None -if "ppt_topic" not in st.session_state: - st.session_state.ppt_topic = "" -if "show_word_download" not in st.session_state: - st.session_state.show_word_download = False -if "word_data" not in st.session_state: - st.session_state.word_data = None -if "word_topic" not in st.session_state: - st.session_state.word_topic = "" -if "show_excel_download" not in st.session_state: - st.session_state.show_excel_download = False -if "excel_data" not in st.session_state: - st.session_state.excel_data = None -if "excel_topic" not in st.session_state: - st.session_state.excel_topic = "" -if "show_csv_download" not in st.session_state: - st.session_state.show_csv_download = False -if "csv_data" not in st.session_state: - st.session_state.csv_data = None -if "csv_topic" not in st.session_state: - st.session_state.csv_topic = "" -if "last_document_topic" not in st.session_state: - st.session_state.last_document_topic = "" -if "last_ppt_topic" not in st.session_state: - st.session_state.last_ppt_topic = "" -if "last_ppt_content" not in st.session_state: - st.session_state.last_ppt_content = "" -if "last_excel_topic" not in st.session_state: - st.session_state.last_excel_topic = "" -if "last_excel_data" not in st.session_state: - st.session_state.last_excel_data = None - -# ============================================ -# SEARCH FUNCTIONS -# ============================================ -def internet_search(query): - try: - clean_query = query.strip() +# ============================================================================ +# 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() - if any(x in clean_query.lower() for x in ["weather", "temperature", "temp"]): - location = clean_query - weather_words = ["weather in", "weather at", "temperature in", "weather", "temperature"] - for word in weather_words: - if word in location.lower(): - location = re.sub(re.escape(word), "", location.lower(), flags=re.IGNORECASE).strip() - break - if location: - weather_url = f"https://wttr.in/{location}?format=%C+%t+%w+%h&m" - weather_response = requests.get(weather_url, timeout=10) - if weather_response.status_code == 200: - weather_data = weather_response.text.strip() - if weather_data and "Unknown" not in weather_data: - return f"Current weather in {location}: {weather_data}" + parsed = self.instruction_parser.parse(instruction, document) - 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 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) + ) - if response.status_code == 200: - results = re.findall(r'([^<]+)', response.text) - snippets = re.findall(r']*>([^<]+)', response.text) + 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() - 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 "" + 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})"] + } + -def get_current_news(): +# ============================================================================ +# FILE PROCESSING FUNCTIONS +# ============================================================================ + +def extract_text_from_pdf(file): try: - url = "https://rss2json.com/api.json?rss_url=https://feeds.bbci.co.uk/news/rss.xml" - response = requests.get(url, timeout=10) - if response.status_code == 200: - data = response.json() - items = data.get("items", [])[:3] - news_text = "LATEST NEWS HEADLINES:\n\n" - for item in items: - news_text += f"- {item.get('title', '')}\n" - news_text += f" {item.get('description', '')[:150]}...\n\n" - return news_text[:1000] - except: - pass - return "" + 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)}" -# ============================================ -# CALCULATOR -# ============================================ +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 calculator(query): +def extract_text_from_txt(file): try: - expression = query.lower() - expression = expression.replace("Γ", "*").replace("x", "*") - numbers = re.findall(r"[0-9\+\-\*\/\.\(\) ]+", expression) - if numbers: - result = eval(numbers[0]) - return str(result) - except: - return None + 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)}" -# ============================================ -# WEB SCRAPING -# ============================================ +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 scrape_webpage(url): +def extract_text_from_json(file): try: - headers = {"User-Agent": "Mozilla/5.0"} - response = requests.get(url, headers=headers, timeout=15) - if response.status_code == 200: - soup = BeautifulSoup(response.content, 'html.parser') - for element in soup(["script", "style", "nav", "footer"]): - element.decompose() - text = soup.get_text() - lines = (line.strip() for line in text.splitlines()) - text = ' '.join(line for line in lines if line) - return text[:3000] if len(text) > 200 else None - except: - pass - return None + 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 extract_urls_from_query(query): - url_pattern = r'https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+[^\s]*' - return re.findall(url_pattern, query) +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}" -# ============================================ -# WEATHER FUNCTIONS -# ============================================ +# ============================================================================ +# DOCUMENT GENERATION FUNCTIONS +# ============================================================================ -def get_weather_comprehensive(location): +def create_ppt_from_content(title, content, filename="presentation"): try: - location = location.strip().replace(" ", "%20") - - current_url = f"https://wttr.in/{location}?format=%C+%t+%w+%h+%H+%l&m" - current_response = requests.get(current_url, timeout=10) + 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')}" - forecast_url = f"https://wttr.in/{location}?0T&m" - forecast_response = requests.get(forecast_url, timeout=10) + content_slide_layout = prs.slide_layouts[1] + lines = content.split('\n') + current_slide = None + current_text_frame = None - if current_response.status_code == 200: - current_data = current_response.text.strip() - - parts = current_data.split() - condition = " ".join(parts[:-4]) if len(parts) > 4 else parts[0] - temp = parts[-4] if len(parts) >= 4 else "N/A" - wind = parts[-3] if len(parts) >= 3 else "N/A" - humidity = parts[-2] if len(parts) >= 2 else "N/A" - - result = f"Weather in {location.title()}\n\n" - result += f"Location: {location.title()}\n" - result += f"Condition: {condition}\n" - result += f"Temperature: {temp}\n" - result += f"Wind: {wind}\n" - result += f"Humidity: {humidity}\n" + for line in lines: + line = line.strip() + if not line: + continue - if forecast_response.status_code == 200: - forecast_text = forecast_response.text - forecast_text = re.sub(r'\x1b\[[0-9;]*m', '', forecast_text) - - lines = forecast_text.split('\n') - forecast_lines = [] - capture = False - for line in lines: - if 'β' in line or 'β' in line or 'β' in line or 'β€' in line or 'β' in line or 'β' in line: - capture = True - if capture and line.strip(): - forecast_lines.append(line) - if len(forecast_lines) > 15: - break + 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 forecast_lines: - result += "\nForecast:\n" - result += '\n'.join(forecast_lines[:10]) - - result += "\n\n*Data from wttr.in*" - return result - return None + 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 get_weather_simple(location): +def create_word_from_content(title, content, filename="document"): try: - location = location.strip().replace(" ", "%20") - url = f"https://wttr.in/{location}?format=%C+%t+%w+%h&m" - response = requests.get(url, timeout=10) + 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() - if response.status_code == 200: - weather_data = response.text.strip() - if weather_data and "Unknown" not in weather_data: - parts = weather_data.split() - condition = " ".join(parts[:-3]) if len(parts) > 3 else parts[0] - temp = parts[-3] if len(parts) >= 3 else "N/A" - wind = parts[-2] if len(parts) >= 2 else "N/A" - humidity = parts[-1] if len(parts) >= 1 else "N/A" - - result = f"Weather in {location.title()}\n\n" - result += f"Condition: {condition}\n" - result += f"Temperature: {temp}\n" - result += f"Wind: {wind}\n" - result += f"Humidity: {humidity}\n" - return result - return None + 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 -# ============================================ -# CODING SEARCH FUNCTIONS -# ============================================ - -def search_coding_solution(query): - search_queries = [ - f"{query} stack overflow", - f"{query} example code", - f"{query} best practice", - f"{query} github" - ] - - all_results = "" - - for search_q in search_queries[:2]: - result = internet_search(search_q) - if result: - all_results += result + "\n\n" - - return all_results - -def search_coding_solution_cached(query): - cache_key = query.lower().strip() - - if cache_key in st.session_state.code_search_cache: - return st.session_state.code_search_cache[cache_key] - - result = search_coding_solution(query) - st.session_state.code_search_cache[cache_key] = result - return result - -def coding_assistant_with_search(query, context=""): - with st.spinner("Searching the internet for the best solution..."): - search_results = search_coding_solution_cached(query) - - coding_prompt = f""" -You are an expert programmer. Generate the best possible code based on the user's request. - -USER REQUEST: {query} - -## INTERNET SEARCH RESULTS (Use these as reference): -{search_results[:3000]} - -## REQUIREMENTS: -- Code must be complete and runnable -- Include all imports -- Add comments -- Handle edge cases - -Generate the best possible code now: -""" - - messages = [ - {"role": "system", "content": "You are an expert programming assistant. Use search results to find the best solution."}, - {"role": "user", "content": coding_prompt} - ] - - return clean_answer(llm(messages)) - -# ============================================ -# ROUTER FUNCTION -# ============================================ - -def route(query): - q = query.lower() - - if any(x in q for x in ["export chat", "save chat", "download chat", "export conversation"]): - return "export_chat" - - if extract_urls_from_query(query): - return "scrape_url" - - file_keywords = ["document", "file", "upload", "pdf", "docx", "txt", "csv", "json", "what is this", "summarize"] - if any(x in q for x in file_keywords): - return "file_task" - - comparison_keywords = ["compare", "comparison", "difference", "similarities"] - if any(x in q for x in comparison_keywords): - return "compare_files" - - if any(x in q for x in ["weather", "temperature", "temp", "rain", "snow", "forecast", "humidity", "wind"]): - return "weather" - - edit_keywords = ["make it", "make the", "change it", "change the", "turn it", "add a", "remove", "edit image", "modify image"] - if any(x in q for x in edit_keywords): - return "edit_image" - - if any(x in q for x in ["generate image", "create image", "draw", "make an image", "picture of", "image of"]): - return "generate_image" - - if any(phrase in q for phrase in ["can you", "do you", "are you able to"]): - return "reason" - - if any(x in q for x in ["who is", "tell me about", "what is", "news", "headlines"]): - return "search" - - if any(x in q for x in ["+", "-", "*", "/", "calculate"]): - return "calculator" - - if any(x in q for x in ["time", "date", "today"]): - return "datetime" - - coding_keywords = ["code", "python", "javascript", "html", "css", "react", "tkinter", "function", "class", "import", "algorithm", "debug", "fix", "write a program", "create a script"] - if any(x in q for x in coding_keywords): - return "coding_with_search" - - factual_keywords = ["president", "current", "elected", "prime minister", "leader", "ceo of"] - if any(x in q for x in factual_keywords): - return "search" - - return "reason" - -# ============================================ -# CLEAN ANSWER -# ============================================ - -def clean_answer(text): - text = text.split("π§ ")[0] - text = text.split("Plan:")[0] - text = text.split("Thinking:")[0] - return text.strip() +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 -# ============================================ -# REASONING FUNCTION -# ============================================ +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 reason(question, context): - memory_context = retrieve_memory(question) - enhanced_context = context - if memory_context: - enhanced_context += "\n" + memory_context - - history_text = "" - if st.session_state.chat_history: - history_text = "PREVIOUS CONVERSATION:\n" - last_exchanges = st.session_state.chat_history[-8:] if len(st.session_state.chat_history) > 8 else st.session_state.chat_history - for role, msg in last_exchanges: - if role == "user": - history_text += f"User: {msg}\n" - else: - history_text += f"Assistant: {msg}\n" - history_text += "\n" +def export_chat_history(): + """FIX #10: Enhanced export includes intent + timeline""" + if not st.session_state.chat_history: + return None - messages = [ - {"role": "system", "content": SYSTEM_PROMPT}, - {"role": "user", "content": f""" -{history_text} - -{enhanced_context[:3000]} - -USER QUESTION: {question} - -ANSWER: -"""} - ] - return clean_answer(llm(messages)) - -# ============================================ -# FILE FUNCTIONS -# ============================================ - -def compare_files(query, file_context, filenames): - prompt = f"Files: {filenames}\n\nContent: {file_context[:4000]}\n\nQuestion: {query}\n\nCompare the files." - messages = [{"role": "system", "content": "You compare files."}, {"role": "user", "content": prompt}] - return clean_answer(llm(messages)) - -def analyze_uploaded_files(query, file_context, filenames): - prompt = f"Files: {filenames}\n\nContent: {file_context[:6000]}\n\nQuestion: {query}\n\nAnswer based on file content." - messages = [{"role": "system", "content": "You analyze files."}, {"role": "user", "content": prompt}] - return clean_answer(llm(messages)) + 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 -def evaluate_work(question, file_context): - prompt = f"Content: {file_context[:3000]}\n\nRequest: {question}\n\nProvide assessment." - messages = [{"role": "system", "content": "You evaluate work."}, {"role": "user", "content": prompt}] - return clean_answer(llm(messages)) +# ============================================================================ +# WEB & UTILITY FUNCTIONS +# ============================================================================ -# ============================================ -# IMAGE GENERATION 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 generate_image(prompt): +def internet_search(query): try: - enhanced_prompt = f"{prompt}, high quality, detailed, well-proportioned, realistic, no distortions, clear features" - negative_prompt = "ugly, deformed, blurry, bad anatomy, extra limbs, extra fingers, distorted face, low quality, messy" - - encoded_prompt = requests.utils.quote(enhanced_prompt) - encoded_negative = requests.utils.quote(negative_prompt) + 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) - timestamp = int(time.time()) - image_url = f"https://image.pollinations.ai/prompt/{encoded_prompt}?width=1024&height=1024&nologo=true&seed={timestamp}&negative={encoded_negative}" - return image_url - except Exception as e: - return None + if response.status_code == 200: + results = re.findall(r'([^<]+)', response.text) + snippets = re.findall(r']*>([^<]+)', 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: - style_prompts = { - "realistic": "photorealistic, high resolution, detailed textures, natural lighting", - "anime": "anime style, clean lines, vibrant colors, well-proportioned", - "cartoon": "cartoon style, smooth lines, cute, well-drawn", - "abstract": "abstract art, creative, artistic, visually appealing" - } - - quality_prompts = { - "high": "4K, highly detailed, sharp focus, professional quality", - "medium": "good quality, clear details, well-rendered", - "fast": "decent quality, recognizable features" - } - - style_enhancement = style_prompts.get(style, style_prompts["realistic"]) - quality_enhancement = quality_prompts.get(quality, quality_prompts["high"]) - - enhanced_prompt = f"{prompt}, {quality_enhancement}, {style_enhancement}" - negative_prompt = "ugly, deformed, blurry, bad anatomy, extra limbs, extra fingers, distorted face, low quality, messy, watermark, text, signature, cropped, out of frame, duplicate, morbid, mutilated, disfigured, gross proportions, malformed limbs, missing arms, missing legs, extra arms, extra legs, fused fingers, too many fingers, mutated hands, poorly drawn hands, bad proportions, cloned face, deformed, disfigured, draft, blurry, grain, low-res, bad art" - + enhanced_prompt = f"{prompt}, high quality, detailed" encoded_prompt = requests.utils.quote(enhanced_prompt) - encoded_negative = requests.utils.quote(negative_prompt) - timestamp = int(time.time()) - image_url = f"https://image.pollinations.ai/prompt/{encoded_prompt}?width=1024&height=1024&nologo=true&seed={timestamp}&negative={encoded_negative}" + 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, quality="high", style="realistic") - + image_url = generate_image_with_quality(prompt) if image_url: - if is_edit: - return f"Edited Image - New Prompt: '{prompt}'\n\n\n\n*Image generated by AI*" - else: - return f"Generated Image for: '{prompt}'\n\n\n\n*Image generated by AI*" + return f"Generated Image for: '{prompt}'\n\n" else: - return "Sorry, I couldn't generate an image right now." - -# ============================================ -# RUN AGENT FUNCTION -# ============================================ + return "Sorry, I couldn't generate an image." -def run_agent(query): - q = query.lower().strip() +def llm_with_fallback(messages, max_retries=2): + models_to_try = [ + "llama-3.3-70b-versatile", + "llama-3.1-70b-versatile", + "mixtral-8x7b-32768" + ] - reset_phrases = ["leave the document", "clear context", "forget the file", "start fresh", "clear files", "new chat"] - if any(phrase in q for phrase in reset_phrases): - st.session_state.file_context = "" - st.session_state.uploaded_files = {} - st.session_state.last_search_query = None - st.session_state.last_search_results = None - st.session_state.last_topic = None - st.session_state.last_image_prompt = None - st.session_state.last_document_topic = "" - st.session_state.last_ppt_topic = "" - st.session_state.last_ppt_content = "" - st.session_state.last_excel_topic = "" - st.session_state.last_excel_data = None - st.session_state.show_csv_download = False - st.session_state.csv_data = None - st.session_state.show_excel_download = False - st.session_state.excel_data = None - return "Context cleared! How can I help you today?" + 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 - # What is a word? check - if q == "what is a word" or q == "what is a word?": - return """A **Word document** (.docx) is a file format created by Microsoft Word. When I say "make a word file", I mean generating a downloadable .docx file. + 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() + -To create one, try: -- "make a word about dogs" -- "create a word document about Python programming" +# ============================================================================ +# ENHANCED RUN AGENT WITH INTELLIGENCE CORE +# ============================================================================ -The file will appear as a download button after I generate it!""" +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 - # Check for "excel" as verb (life advice) first - if any(phrase in q for phrase in ["excel in", "excel at", "how to excel", "excel in life", "excel at work", "excel in school"]): - return reason(query, get_current_datetime()) + 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 + ) - # Excel/Spreadsheet generation (.xlsx) - only for file creation - excel_file_keywords = [ - "excel file", "excel spreadsheet", "excel sheet", "xlsx", - "generate an excel", "make an excel", "create an excel", - "generate a spreadsheet", "make a spreadsheet", "create a spreadsheet", - "excel about", "spreadsheet about", "excel with data" - ] + 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() - is_excel_file_request = any(phrase in q for phrase in excel_file_keywords) - is_file_creation = any(word in q for word in ["generate", "make", "create", "build"]) and "excel" in q and "how to" not in q + # Document workspace commands + if q.startswith("/"): + return handle_document_command(q) - if is_excel_file_request or is_file_creation: - - # Determine topic - topic = "Phone_Sales_Report" - if "phone sales" in q: - topic = "Phone_Sales_Report" - elif "student" in q: - topic = "Student_Data" - elif "product" in q: - topic = "Product_Inventory" - else: - if "about" in q: - topic_part = q.split("about")[-1].strip().replace(" ", "_") - if topic_part and len(topic_part) > 2 and topic_part not in ["it", "the", "a", "an"]: - topic = topic_part[:30] - - st.session_state.last_excel_topic = topic - - with st.spinner(f"Creating Excel (.xlsx) file: {topic}..."): - - # Create data based on topic - if "phone" in topic.lower() or "sales" in topic.lower(): - data_rows = [ - ["Date", "Sales Rep", "Region", "Phone Model", "Quantity", "Unit Price", "Total Sales"], - ["2024-01-01", "John Smith", "North", "iPhone 15 Pro", 5, 999, 4995], - ["2024-01-02", "Jane Doe", "South", "Samsung Galaxy S24", 3, 899, 2697], - ["2024-01-03", "John Smith", "North", "Google Pixel 8", 4, 699, 2796], - ["2024-01-04", "Bob Wilson", "East", "iPhone 15", 6, 799, 4794], - ["2024-01-05", "Jane Doe", "South", "Samsung Galaxy S24+", 2, 1099, 2198], - ["2024-01-06", "Alice Brown", "West", "iPhone 15 Pro Max", 3, 1199, 3597], - ["2024-01-07", "John Smith", "North", "Samsung Galaxy Z Flip5", 2, 999, 1998], - ["2024-01-08", "Bob Wilson", "East", "Google Pixel 8 Pro", 3, 899, 2697], - ["2024-01-09", "Jane Doe", "South", "iPhone 15", 4, 799, 3196], - ["2024-01-10", "Alice Brown", "West", "Samsung Galaxy S24", 5, 899, 4495] - ] - total = sum(row[6] for row in data_rows[1:]) - data_rows.append(["", "", "", "", "TOTAL:", "", total]) - - elif "student" in topic.lower(): - data_rows = [ - ["Student ID", "Name", "Grade", "Subject", "Score", "Attendance"], - ["S001", "Emma Watson", "10th", "Mathematics", 95, "98%"], - ["S002", "Liam Chen", "10th", "Science", 88, "95%"], - ["S003", "Sophia Patel", "11th", "English", 92, "100%"], - ["S004", "Noah Kim", "9th", "History", 85, "92%"], - ["S005", "Olivia Jones", "12th", "Physics", 91, "97%"] - ] - else: - data_rows = [ - ["Category", "Item", "Value", "Status"], - ["Research", "Market Analysis", 85, "Completed"], - ["Development", "Feature Dev", 70, "In Progress"], - ["Testing", "QA Testing", 92, "Completed"], - ["Deployment", "Release", 45, "Pending"] - ] - - 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 - st.session_state.last_excel_data = data_rows - return f"I've created a REAL Excel (.xlsx) file: {topic}. Scroll down to download it!" - else: - csv_data = create_csv_from_data(topic, data_rows) - if csv_data: - st.session_state.csv_data = csv_data - st.session_state.csv_topic = topic - st.session_state.show_csv_download = True - return f"Excel creation failed, but I've created a CSV file: {topic}. Scroll down to download it!" - else: - return "Sorry, I couldn't create the file. Please try again." + # Check for document editing commands + edit_keywords = ["improve", "rewrite", "summarize", "expand", "shorten", + "fix grammar", "make formal", "make academic", "translate"] - # Direct PPT generation - ppt_commands = [ - "make a ppt", "make a powerpoint", "create a ppt", "create a powerpoint", - "generate a ppt", "generate a powerpoint", "build a ppt", "build a powerpoint" - ] - if any(phrase in q for phrase in ppt_commands): - topic = query - for word in ppt_commands: - if word in topic.lower(): - topic = re.sub(re.escape(word), "", topic.lower(), flags=re.IGNORECASE).strip() - break - topic = topic.strip() or "MozeAI Generated Presentation" - - if topic in ["about it", "it", "about"]: - topic = "Presentation" - - with st.spinner(f"Creating PowerPoint presentation about '{topic}'..."): - content_prompt = f'''Create a PowerPoint presentation about "{topic}" with MULTIPLE SLIDES. + 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()) -Format your response as: + result = handle_intelligent_edit(query, stream_callback=stream_callback, selected_files=selected_files) -Introduction to {topic} -- First main point -- Second important point -- Third key point + # Clarification needed + if result is None: + return "__CLARIFICATION_NEEDED__" -Key Features -- Feature 1 with explanation -- Feature 2 with explanation -- Feature 3 with explanation + 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 -Benefits/Importance -- Benefit 1 -- Benefit 2 -- Benefit 3 + # 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 -Conclusion -- Key takeaway 1 -- Key takeaway 2 -- Key takeaway 3''' + 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 - ai_content = reason(content_prompt, "") - ppt_bytes = create_ppt_from_content(topic, ai_content) + 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 ppt_bytes: - st.session_state.ppt_data = ppt_bytes - st.session_state.ppt_topic = topic - st.session_state.last_ppt_topic = topic - st.session_state.last_ppt_content = ai_content - st.session_state.show_ppt_download = True - return f"I've created a PowerPoint presentation about {topic}. Scroll down to download it!" - else: - return "Sorry, I couldn't create the PowerPoint. Please try again." + 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 - # Direct Word generation - word_commands = [ - "make a word", "make a doc", "create a word", "create a doc", - "generate a word", "generate a doc", "build a word", "build a doc", - "make a document", "create a document" - ] - if any(phrase in q for phrase in word_commands): - topic = query - for word in word_commands: - if word in topic.lower(): - topic = re.sub(re.escape(word), "", topic.lower(), flags=re.IGNORECASE).strip() - break - topic = topic.strip() or "MozeAI Generated Document" - - if topic in ["about it", "it", "about"]: - topic = "Document" + # 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" - st.session_state.last_document_topic = topic + 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] + ] - with st.spinner(f"Creating Word document about '{topic}'..."): - content_prompt = f'Write detailed content for a Word document about "{topic}". Include an engaging title, an introduction paragraph, 3-5 main sections with detailed information, and a conclusion. Make it comprehensive and well-organized, around 500-800 words.' - - ai_content = reason(content_prompt, "") - word_bytes = create_word_from_content(topic, ai_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"I've created a Word document about {topic}. Scroll down to download it!" - else: - return "Sorry, I couldn't create the Word document. Please try again." + 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", "draw", "picture of", "image of"]): - with st.spinner("Generating image..."): - image_prompt = q.replace("generate image of", "").replace("create image of", "").replace("draw a", "").replace("picture of", "").replace("image of", "").strip() - if not image_prompt: - image_prompt = q - st.session_state.last_image_prompt = image_prompt - return generate_and_display_image(image_prompt) - - # Direct responses - if any(phrase in q for phrase in ["who are you", "what are you"]): - return "I'm MozeAI, your AI assistant! Created by Mukiibi Moses. I can generate Excel files, PowerPoint presentations, Word documents, images, and more!" - - if any(phrase in q for phrase in ["who created you", "your creator", "mukiibi moses"]): - return "Mukiibi Moses is my creator, a Computer Engineering student at Kyungdong University in South Korea. Check out his portfolio: https://moze12432.github.io/" - - # Default to search/reason + 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 - answer = reason(query, context) - st.session_state.last_response = answer - store_memory(answer) + st.warning("π€ I need a bit more info to edit your document precisely:") - return answer + 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() -# ============================================ -# UI - MAIN DISPLAY -# ============================================ -st.markdown('
Intelligent AI Assistant
', unsafe_allow_html=True) -st.markdown("---") +# ============================================================================ +# 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"{old_wc} β {new_wc} words ({net:+d})", + unsafe_allow_html=True + ) -with st.sidebar: - st.markdown("### MozeAI") + # Smart rollback controls st.markdown("---") - - if st.button("New Chat", key="new_chat_btn", use_container_width=True): - if not st.session_state.get("is_resetting", False): - st.session_state.is_resetting = True - st.session_state.chat_history = [] - st.session_state.uploaded_files = {} - st.session_state.file_context = "" - st.session_state.last_image_prompt = None - st.session_state.last_search_query = None - st.session_state.last_search_results = None - st.session_state.last_response = None - st.session_state.code_search_cache = {} - st.session_state.show_ppt_download = False - st.session_state.show_word_download = False - st.session_state.show_excel_download = False - st.session_state.show_csv_download = False - st.session_state.is_resetting = False - st.success("New chat started!") + 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"{i}. {label} " + f"{net:+d}w", + 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() - - if st.button("Clear Files", key="clear_files_btn", use_container_width=True): - st.session_state.uploaded_files = {} - st.session_state.file_context = "" - st.success("Files cleared!") - st.rerun() st.markdown("---") - st.markdown("### Upload Files") - uploaded_files = st.file_uploader( - "Choose files", - type=['pdf', 'docx', 'txt', 'csv', 'json'], - accept_multiple_files=True, - key="sidebar_uploader", + content = st.text_area( + "Content", + value=st.session_state.workspace.current_document["content"], + height=400, + key="doc_editor", label_visibility="collapsed" ) - if uploaded_files: - for file in uploaded_files: - if file.name not in st.session_state.uploaded_files: - with st.spinner(f"Processing {file.name}..."): - content = process_uploaded_file(file) - if content and not content.startswith("Error"): - st.session_state.uploaded_files[file.name] = content - st.success(f" {file.name}") + 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") - if st.session_state.uploaded_files: - parts = [] - for name, content in st.session_state.uploaded_files.items(): - parts.append(f"\n{'='*50}\nπ {name}\n{'='*50}\n{content}\n") - st.session_state.file_context = "\n".join(parts) - st.info(f" {len(st.session_state.uploaded_files)} file(s) loaded") - - st.markdown("---") - st.markdown("### Image Generation") - st.markdown("**Generate:** `generate image of a cat`") - st.markdown("**Edit:** `make it black` or `add a hat`") - st.markdown("---") - st.markdown("### Document Generation") - st.markdown("**PPT:** `make a ppt about AI`") - st.markdown("**Word:** `create a word document about Python`") - st.markdown("**Excel:** `generate an excel about phone sales`") - st.markdown("**CSV:** `generate a csv about data`") - st.markdown("---") - st.markdown("### About") - st.markdown("**Creator:** Mukiibi Moses") - st.markdown("**University:** Kyungdong University, South Korea") - if st.session_state.last_model_used: - st.caption(f"Model: {st.session_state.last_model_used}") + # 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("---") - st.markdown("### Export Options") - - if st.button("Export Chat History", key="export_chat_btn", use_container_width=True): - export_content = export_chat_history() - if export_content: - st.download_button( - label="Download (.txt)", - data=export_content, - file_name=f"chat_history_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt", - mime="text/plain", - key="export_download" - ) - st.success("Chat history ready as .txt!") + 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)}") -# ============================================ -# CHAT DISPLAY -# ============================================ -for role, msg in st.session_state.chat_history: - with st.chat_message(role): - st.write(msg) +# ============================================================================ +# 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") -query = st.chat_input("Ask me anything - generate images, documents, search the web, analyze files, code, and more!") + # Show clarification dialog if pending + render_clarification_dialog() -if query: - st.session_state.chat_history.append(("user", query)) - with st.chat_message("user"): - st.write(query) + # Show file suggestions if any + render_file_suggestions() - response = run_agent(query) + # 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**") - with st.chat_message("assistant"): - st.write(response) + query = st.chat_input("Ask me to edit, analyze, or generateβ¦") - st.session_state.chat_history.append(("assistant", response)) - st.rerun() + # 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)) -# ============================================ -# DOWNLOAD BUTTONS (Appear after chat) -# ============================================ + 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)) -# PowerPoint Download -if st.session_state.get("show_ppt_download", False) and st.session_state.get("ppt_data"): - st.markdown("---") - st.success(f" PowerPoint about {st.session_state.ppt_topic} is ready!") - col1, col2, col3 = st.columns([1, 2, 1]) - with col2: + 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", + label="π₯ Download PowerPoint", data=st.session_state.ppt_data, - file_name=f"{st.session_state.ppt_topic.replace(' ', '_')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pptx", - mime="application/vnd.openxmlformats-officedocument.presentationml.presentation", - use_container_width=True + file_name=f"{st.session_state.ppt_topic}.pptx", + mime="application/vnd.openxmlformats-officedocument.presentationml.presentation" ) - st.session_state.show_ppt_download = False - st.session_state.ppt_data = None - -# Word Download -if st.session_state.get("show_word_download", False) and st.session_state.get("word_data"): - st.markdown("---") - st.success(f" Word document about {st.session_state.word_topic} is ready!") - col1, col2, col3 = st.columns([1, 2, 1]) - with col2: + 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", + label="π₯ Download Word Document", data=st.session_state.word_data, - file_name=f"{st.session_state.word_topic.replace(' ', '_')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx", - mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document", - use_container_width=True + file_name=f"{st.session_state.word_topic}.docx", + mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document" ) - st.session_state.show_word_download = False - st.session_state.word_data = None - -# REAL Excel Download (.xlsx) -if st.session_state.get("show_excel_download", False) and st.session_state.get("excel_data"): - st.markdown("---") - st.success(f" Excel (.xlsx) file {st.session_state.excel_topic} is ready!") + st.session_state.show_word_download = False - col1, col2, col3 = st.columns([1, 2, 1]) - with col2: + if st.session_state.get("show_excel_download", False) and st.session_state.get("excel_data"): st.download_button( - label="Download Excel File (.xlsx)", + label="π₯ Download Excel File", data=st.session_state.excel_data, - file_name=f"{st.session_state.excel_topic}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx", - mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - use_container_width=True, - key="excel_download_btn" + file_name=f"{st.session_state.excel_topic}.xlsx", + mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) - st.session_state.show_excel_download = False - st.session_state.excel_data = None + 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 -# CSV Download (fallback) -if st.session_state.get("show_csv_download", False) and st.session_state.get("csv_data"): + # 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(""" + + """, 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('Intelligent Document Workspace
', unsafe_allow_html=True) st.markdown("---") - st.info(f" CSV file {st.session_state.csv_topic} is ready!") - col1, col2, col3 = st.columns([1, 2, 1]) - with col2: - st.download_button( - label="Download CSV File", - data=st.session_state.csv_data, - file_name=f"{st.session_state.csv_topic}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv", - mime="text/csv", - use_container_width=True, - key="csv_download_btn" - ) - st.session_state.show_csv_download = False - st.session_state.csv_data = None \ No newline at end of file + # 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( + 'MozeAI Document Studio | Created by Mukiibi Moses
', + unsafe_allow_html=True + ) + +if __name__ == "__main__": + main() \ No newline at end of file