OverviewFilesCommitsIssuesDocsReleasesPeers

ollama_semantic_function_calling / ollamatools-new / ollamatools / plugins / tools / doc.py

279 lines · python ref:
"""
Documentation Tools for OllamaTools.

This module provides utilities for generating and processing documentation.
It includes tools for creating formatted documentation, extracting relevant
context from documents, and summarizing text.

All functions support integration with the permission system for security.
"""

import markdown
import re
from collections import Counter
from typing import Optional, Dict, Any, List


def generate_markdown(
    content: str,
    title: str,
    format_type: str = 'markdown',
    permission_manager = None
) -> str:
    """
    Generate formatted documentation.
    
    Args:
        content: Document content to format
        title: Document title
        format_type: Output format ('markdown' or 'html')
        permission_manager: Optional permission manager for security checks
        
    Returns:
        str: Formatted documentation
    
    Raises:
        ValueError: If format_type is not supported
        PermissionError: If permission is denied
    """
    # Permission check
    if permission_manager is not None:
        args = {
            'content_preview': content[:50] + ('...' if len(content) > 50 else ''),
            'title': title,
            'format_type': format_type
        }
        if not permission_manager.request_permission('generate_markdown', args):
            raise PermissionError("Permission denied to generate markdown document")
    
    # Input validation
    if not content or not isinstance(content, str):
        raise ValueError("Content must be a non-empty string")
    
    if not title or not isinstance(title, str):
        raise ValueError("Title must be a non-empty string")
    
    if format_type not in ['markdown', 'html']:
        raise ValueError("Format type must be 'markdown' or 'html'")
    
    # Create the formatted document
    if format_type == 'markdown':
        # For markdown, we'll create a simple markdown document with a title
        formatted_doc = f"# {title}\n\n{content}"
        return formatted_doc
    else:  # html
        # For HTML, we'll convert the markdown to HTML
        html_content = markdown.markdown(content)
        formatted_doc = f"""<!DOCTYPE html>
<html>
<head>
    <title>{title}</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
    <h1>{title}</h1>
    {html_content}
</body>
</html>"""
        return formatted_doc


def extract_context(
    query: str,
    documents: List[str],
    max_length: int = 1000,
    permission_manager = None
) -> str:
    """
    Extract relevant context from documents based on a query.
    
    This function extracts passages from documents that are most relevant to the query
    based on keyword matching. It prioritizes sentences containing query keywords.
    
    Args:
        query: Search query to find relevant contexts
        documents: List of document contents
        max_length: Maximum total length of extracted context
        permission_manager: Optional permission manager for security checks
        
    Returns:
        str: Extracted context
        
    Raises:
        ValueError: If inputs are invalid
        PermissionError: If permission is denied
    """
    # Permission check
    if permission_manager is not None:
        args = {
            'query': query,
            'documents_count': len(documents) if documents else 0,
            'max_length': max_length
        }
        if not permission_manager.request_permission('extract_context', args):
            raise PermissionError("Permission denied to extract context from documents")
    
    # Input validation
    if not query or not isinstance(query, str):
        raise ValueError("Query must be a non-empty string")
    
    if not documents or not isinstance(documents, list):
        raise ValueError("Documents must be a non-empty list of strings")
    
    if not all(isinstance(doc, str) for doc in documents):
        raise ValueError("All documents must be strings")
    
    if not max_length or not isinstance(max_length, int) or max_length <= 0:
        raise ValueError("Max length must be a positive integer")
    
    # Extract keywords from the query
    # Normalize by removing common words and punctuation
    query = query.lower()
    query_words = re.findall(r'\b\w+\b', query)
    
    # Remove common stop words
    stop_words = {'the', 'a', 'an', 'and', 'or', 'but', 'is', 'are', 'of', 'to', 'in', 
                 'for', 'with', 'by', 'at', 'on', 'from'}
    keywords = [word for word in query_words if word not in stop_words]
    
    if not keywords:
        # Use the original words if all were stop words
        keywords = query_words
    
    # Find sentences in documents containing keywords
    relevant_sentences = []
    for doc in documents:
        # Split documents into sentences (simple heuristic)
        sentences = re.split(r'(?<=[.!?])\s+', doc)
        
        for sentence in sentences:
            # Check for keyword presence
            sentence_lower = sentence.lower()
            sentence_words = set(re.findall(r'\b\w+\b', sentence_lower))
            
            # Calculate relevance score as the number of keywords found
            keywords_found = sum(1 for keyword in keywords if keyword in sentence_words)
            
            if keywords_found > 0:
                relevant_sentences.append((sentence, keywords_found))
    
    # Sort sentences by relevance (number of keywords found)
    relevant_sentences.sort(key=lambda x: x[1], reverse=True)
    
    # Build context up to max_length
    context = []
    current_length = 0
    
    for sentence, _ in relevant_sentences:
        if current_length + len(sentence) + 1 <= max_length:  # +1 for space
            context.append(sentence)
            current_length += len(sentence) + 1
        else:
            # If we can't fit the next sentence, stop
            break
    
    # If no relevant sentences found, return empty string
    if not context:
        return ""
    
    return " ".join(context)


def summarize_text(
    text: str,
    max_length: int = 500,
    permission_manager = None
) -> str:
    """
    Generate a summary of text using extractive summarization.
    
    This function extracts the most important sentences from the text
    based on word frequency analysis.
    
    Args:
        text: Text to summarize
        max_length: Maximum summary length
        permission_manager: Optional permission manager for security checks
        
    Returns:
        str: Text summary
        
    Raises:
        ValueError: If inputs are invalid
        PermissionError: If permission is denied
    """
    # Permission check
    if permission_manager is not None:
        args = {
            'text_preview': text[:50] + ('...' if len(text) > 50 else ''),
            'text_length': len(text),
            'max_length': max_length
        }
        if not permission_manager.request_permission('summarize_text', args):
            raise PermissionError("Permission denied to summarize text")
    
    # Input validation
    if not text or not isinstance(text, str):
        raise ValueError("Text must be a non-empty string")
    
    if not max_length or not isinstance(max_length, int) or max_length <= 0:
        raise ValueError("Max length must be a positive integer")
    
    # If text is already shorter than max_length, return it as is
    if len(text) <= max_length:
        return text
    
    # Split text into sentences
    sentences = re.split(r'(?<=[.!?])\s+', text)
    
    # Handle case with only one sentence
    if len(sentences) <= 1:
        return text[:max_length] + "..." if len(text) > max_length else text
    
    # Extract all words, lowercase them, and remove stop words
    stop_words = {'the', 'a', 'an', 'and', 'or', 'but', 'is', 'are', 'of', 'to', 'in', 
                 'for', 'with', 'by', 'at', 'on', 'from'}
    
    words = []
    for sentence in sentences:
        sentence_words = re.findall(r'\b\w+\b', sentence.lower())
        words.extend([word for word in sentence_words if word not in stop_words])
    
    # Count word frequencies
    word_freq = Counter(words)
    
    # Score sentences based on word frequencies
    sentence_scores = []
    for i, sentence in enumerate(sentences):
        sentence_words = re.findall(r'\b\w+\b', sentence.lower())
        score = sum(word_freq[word] for word in sentence_words if word not in stop_words)
        # Normalize by sentence length to avoid bias towards longer sentences
        if len(sentence_words) > 0:
            score = score / len(sentence_words)
        sentence_scores.append((i, sentence, score))
    
    # Sort sentences by score
    sentence_scores.sort(key=lambda x: x[2], reverse=True)
    
    # Select top sentences and restore original order
    top_sentences = sentence_scores[:len(sentences)//3 + 1]  # Take ~1/3 of sentences
    top_sentences.sort(key=lambda x: x[0])  # Sort by original position
    
    # Build summary up to max_length
    summary = []
    current_length = 0
    
    for _, sentence, _ in top_sentences:
        if current_length + len(sentence) + 1 <= max_length:  # +1 for space
            summary.append(sentence)
            current_length += len(sentence) + 1
        else:
            break
    
    # If no sentences fit, take the first sentence and truncate
    if not summary:
        return sentences[0][:max_length] + "..."
    
    return " ".join(summary)