methods implemented
This commit is contained in:
parent
03c17d7506
commit
b907d068b3
26 changed files with 6291 additions and 1101 deletions
1039
modules/historic_data_agents/agentCoder.py
Normal file
1039
modules/historic_data_agents/agentCoder.py
Normal file
File diff suppressed because it is too large
Load diff
537
modules/historic_data_agents/agentDocumentation.py
Normal file
537
modules/historic_data_agents/agentDocumentation.py
Normal file
|
|
@ -0,0 +1,537 @@
|
|||
"""
|
||||
Documentation agent for generating structured documentation.
|
||||
Provides comprehensive documentation generation capabilities.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
import os
|
||||
import hashlib
|
||||
import base64
|
||||
import uuid
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import traceback
|
||||
import sys
|
||||
import importlib.util
|
||||
import inspect
|
||||
from pydantic import BaseModel
|
||||
|
||||
from modules.workflow.agentBase import AgentBase
|
||||
from modules.interfaces.serviceChatModel import ChatContent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AgentDocumentation(AgentBase):
|
||||
"""AI-driven agent for creating documentation and structured content using multi-step generation"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the documentation agent"""
|
||||
super().__init__()
|
||||
self.name = "documentation"
|
||||
self.label = "Documentation"
|
||||
self.description = "Creates structured documentation, reports, and content using AI with multi-step generation"
|
||||
self.capabilities = [
|
||||
"report_generation",
|
||||
"documentation",
|
||||
"content_structuring",
|
||||
"technical_writing",
|
||||
"knowledge_organization"
|
||||
]
|
||||
|
||||
def setDependencies(self, serviceBase=None):
|
||||
"""Set external dependencies for the agent."""
|
||||
self.setService(serviceBase)
|
||||
|
||||
async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Process a task by focusing on required outputs and using AI to generate them.
|
||||
|
||||
Args:
|
||||
task: Task dictionary with prompt, inputDocuments, outputSpecifications
|
||||
|
||||
Returns:
|
||||
Dictionary with feedback and documents
|
||||
"""
|
||||
try:
|
||||
# Extract task information
|
||||
prompt = task.get("prompt", "")
|
||||
inputDocuments = task.get("inputDocuments", [])
|
||||
outputSpecs = task.get("outputSpecifications", [])
|
||||
|
||||
# Check AI service
|
||||
if not self.service or not self.service.base:
|
||||
return {
|
||||
"feedback": "The Documentation agent requires an AI service to function.",
|
||||
"documents": []
|
||||
}
|
||||
|
||||
# Extract context from input documents - focusing only on dataExtracted
|
||||
documentContext = self._extractDocumentContext(inputDocuments)
|
||||
|
||||
# Create task analysis to understand the requirements
|
||||
documentationPlan = await self._analyzeTask(prompt, documentContext, outputSpecs)
|
||||
logger.debug(f"Documentation plan: {documentationPlan}")
|
||||
|
||||
# Generate all required output documents
|
||||
documents = []
|
||||
|
||||
# If no output specs provided, create default document
|
||||
if not outputSpecs:
|
||||
defaultFormat = documentationPlan.get("recommendedFormat", "markdown")
|
||||
defaultTitle = documentationPlan.get("title", "Documentation")
|
||||
safeTitle = self._sanitizeFilename(defaultTitle)
|
||||
|
||||
outputSpecs = [
|
||||
{"label": f"{safeTitle}.{defaultFormat}", "description": "Comprehensive documentation"}
|
||||
]
|
||||
|
||||
# Process each output specification
|
||||
for spec in outputSpecs:
|
||||
outputLabel = spec.get("label", "")
|
||||
outputDescription = spec.get("description", "")
|
||||
|
||||
# Generate the document using multi-step approach
|
||||
document = await self._createDocumentMultiStep(
|
||||
prompt,
|
||||
documentContext,
|
||||
outputLabel,
|
||||
outputDescription,
|
||||
documentationPlan
|
||||
)
|
||||
|
||||
documents.append(document)
|
||||
|
||||
# Generate feedback
|
||||
feedback = documentationPlan.get("feedback", f"Created {len(documents)} documents based on your requirements.")
|
||||
|
||||
return {
|
||||
"feedback": feedback,
|
||||
"documents": documents
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in documentation generation: {str(e)}", exc_info=True)
|
||||
return {
|
||||
"feedback": f"Error during documentation generation: {str(e)}",
|
||||
"documents": []
|
||||
}
|
||||
|
||||
def _extractDocumentContext(self, documents: List[Dict[str, Any]]) -> str:
|
||||
"""
|
||||
Extract context from input documents, focusing on dataExtracted.
|
||||
|
||||
Args:
|
||||
documents: List of document objects
|
||||
|
||||
Returns:
|
||||
Extracted context as text
|
||||
"""
|
||||
contextParts = []
|
||||
|
||||
for doc in documents:
|
||||
docName = doc.get("name", "unnamed")
|
||||
if doc.get("ext"):
|
||||
docName = f"{docName}.{doc.get('ext')}"
|
||||
|
||||
contextParts.append(f"\n\n--- {docName} ---\n")
|
||||
|
||||
# Process contents for dataExtracted
|
||||
for content in doc.get("contents", []):
|
||||
if content.get("dataExtracted"):
|
||||
contextParts.append(content.get("dataExtracted", ""))
|
||||
|
||||
return "\n".join(contextParts)
|
||||
|
||||
def _sanitizeFilename(self, filename: str) -> str:
|
||||
"""
|
||||
Sanitize a filename by removing invalid characters.
|
||||
|
||||
Args:
|
||||
filename: Filename to sanitize
|
||||
|
||||
Returns:
|
||||
Sanitized filename
|
||||
"""
|
||||
# Replace invalid characters with underscores
|
||||
invalidChars = r'<>:"/\|?*'
|
||||
for char in invalidChars:
|
||||
filename = filename.replace(char, '_')
|
||||
|
||||
# Trim filename if too long
|
||||
if len(filename) > 100:
|
||||
filename = filename[:97] + "..."
|
||||
|
||||
return filename
|
||||
|
||||
async def _analyzeTask(self, prompt: str, context: str, outputSpecs: List) -> Dict:
|
||||
"""
|
||||
Use AI to analyze the task and create a documentation plan.
|
||||
|
||||
Args:
|
||||
prompt: The task prompt
|
||||
context: Document context
|
||||
outputSpecs: Output specifications
|
||||
|
||||
Returns:
|
||||
Documentation plan dictionary
|
||||
"""
|
||||
analysisPrompt = f"""
|
||||
Analyze this documentation task and create a detailed plan.
|
||||
|
||||
TASK: {prompt}
|
||||
|
||||
DOCUMENT CONTEXT SAMPLE:
|
||||
{context[:1000]}... (truncated)
|
||||
|
||||
OUTPUT REQUIREMENTS:
|
||||
{json.dumps(outputSpecs, indent=2)}
|
||||
|
||||
Create a detailed documentation plan in JSON format with the following structure:
|
||||
{{
|
||||
"title": "Document Title",
|
||||
"documentType": "report|manual|guide|whitepaper|etc",
|
||||
"audience": "technical|general|executive|etc",
|
||||
"detailedStructure": [
|
||||
{{
|
||||
"title": "Chapter/Section Title",
|
||||
"keyPoints": ["point1", "point2", ...],
|
||||
"subsections": ["subsection1", "subsection2", ...],
|
||||
"importance": "high|medium|low",
|
||||
"estimatedLength": "short|medium|long"
|
||||
}},
|
||||
... more sections ...
|
||||
],
|
||||
"keyTopics": ["topic1", "topic2", ...],
|
||||
"tone": "formal|conversational|instructional|etc",
|
||||
"recommendedFormat": "markdown|html|text|etc",
|
||||
"formattingRequirements": ["requirement1", "requirement2", ...],
|
||||
"executiveSummary": "Brief description of what the document will cover",
|
||||
"feedback": "Brief message explaining the documentation approach"
|
||||
}}
|
||||
|
||||
Only return valid JSON. No preamble or explanations.
|
||||
"""
|
||||
|
||||
try:
|
||||
response = await self.service.base.callAi([
|
||||
{"role": "system", "content": "You are a documentation expert. Respond with valid JSON only."},
|
||||
{"role": "user", "content": analysisPrompt}
|
||||
])
|
||||
|
||||
# Extract JSON from response
|
||||
jsonStart = response.find('{')
|
||||
jsonEnd = response.rfind('}') + 1
|
||||
|
||||
if jsonStart >= 0 and jsonEnd > jsonStart:
|
||||
plan = json.loads(response[jsonStart:jsonEnd])
|
||||
return plan
|
||||
else:
|
||||
# Fallback if JSON not found
|
||||
return {
|
||||
"title": "Documentation (DEFAULT)",
|
||||
"documentType": "report",
|
||||
"audience": "general",
|
||||
"detailedStructure": [
|
||||
{
|
||||
"title": "Introduction",
|
||||
"keyPoints": ["Purpose", "Scope"],
|
||||
"subsections": [],
|
||||
"importance": "high",
|
||||
"estimatedLength": "short"
|
||||
},
|
||||
{
|
||||
"title": "Main Content",
|
||||
"keyPoints": ["Core Information"],
|
||||
"subsections": ["Key Findings", "Analysis"],
|
||||
"importance": "high",
|
||||
"estimatedLength": "long"
|
||||
},
|
||||
{
|
||||
"title": "Conclusion",
|
||||
"keyPoints": ["Summary", "Next Steps"],
|
||||
"subsections": [],
|
||||
"importance": "medium",
|
||||
"estimatedLength": "short"
|
||||
}
|
||||
],
|
||||
"keyTopics": ["General Information"],
|
||||
"tone": "formal",
|
||||
"recommendedFormat": "markdown",
|
||||
"formattingRequirements": ["Clear headings", "Professional formatting"],
|
||||
"executiveSummary": "A comprehensive documentation covering the requested topics.",
|
||||
"feedback": "Created documentation based on your requirements."
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error creating documentation plan: {str(e)}")
|
||||
return {
|
||||
"title": "Documentation",
|
||||
"documentType": "report",
|
||||
"audience": "general",
|
||||
"detailedStructure": [
|
||||
{
|
||||
"title": "Introduction",
|
||||
"keyPoints": ["Purpose", "Scope"],
|
||||
"subsections": [],
|
||||
"importance": "high",
|
||||
"estimatedLength": "short"
|
||||
},
|
||||
{
|
||||
"title": "Main Content",
|
||||
"keyPoints": ["Core Information"],
|
||||
"subsections": ["Key Findings", "Analysis"],
|
||||
"importance": "high",
|
||||
"estimatedLength": "long"
|
||||
},
|
||||
{
|
||||
"title": "Conclusion",
|
||||
"keyPoints": ["Summary", "Next Steps"],
|
||||
"subsections": [],
|
||||
"importance": "medium",
|
||||
"estimatedLength": "short"
|
||||
}
|
||||
],
|
||||
"keyTopics": ["General Information"],
|
||||
"tone": "formal",
|
||||
"recommendedFormat": "markdown",
|
||||
"formattingRequirements": ["Clear headings", "Professional formatting"],
|
||||
"executiveSummary": "A comprehensive documentation covering the requested topics.",
|
||||
"feedback": "Created documentation based on your requirements."
|
||||
}
|
||||
|
||||
async def _createDocumentMultiStep(self, prompt: str, context: str, outputLabel: str,
|
||||
outputDescription: str, documentationPlan: Dict) -> ChatContent:
|
||||
"""
|
||||
Create a document using a multi-step approach with separate AI calls for each section.
|
||||
|
||||
Args:
|
||||
prompt: Original task prompt
|
||||
context: Document context
|
||||
outputLabel: Output filename
|
||||
outputDescription: Description of desired output
|
||||
documentationPlan: Documentation plan from AI
|
||||
|
||||
Returns:
|
||||
ChatContent object
|
||||
"""
|
||||
try:
|
||||
# Determine format from filename
|
||||
formatType = outputLabel.split('.')[-1].lower() if '.' in outputLabel else "md"
|
||||
|
||||
# Map format to contentType
|
||||
contentTypeMap = {
|
||||
"md": "text/markdown",
|
||||
"markdown": "text/markdown",
|
||||
"html": "text/html",
|
||||
"txt": "text/plain",
|
||||
"text": "text/plain",
|
||||
"json": "application/json",
|
||||
"csv": "text/csv"
|
||||
}
|
||||
|
||||
contentType = contentTypeMap.get(formatType, "text/plain")
|
||||
|
||||
# Get document information
|
||||
title = documentationPlan.get("title", "Documentation")
|
||||
documentType = documentationPlan.get("documentType", "document")
|
||||
audience = documentationPlan.get("audience", "general")
|
||||
tone = documentationPlan.get("tone", "formal")
|
||||
keyTopics = documentationPlan.get("keyTopics", [])
|
||||
formattingRequirements = documentationPlan.get("formattingRequirements", [])
|
||||
|
||||
# Get the detailed structure
|
||||
detailedStructure = documentationPlan.get("detailedStructure", [])
|
||||
|
||||
# Step 1: Generate executive summary
|
||||
summaryPrompt = f"""
|
||||
Create an executive summary for a {documentType} titled "{title}".
|
||||
|
||||
DOCUMENT OVERVIEW:
|
||||
- Type: {documentType}
|
||||
- Audience: {audience}
|
||||
- Key Topics: {', '.join(keyTopics)}
|
||||
|
||||
TASK CONTEXT: {prompt}
|
||||
|
||||
The executive summary should:
|
||||
1. Provide a concise overview of the document's purpose
|
||||
2. Highlight key points and findings
|
||||
3. Be clear and engaging for the target audience
|
||||
4. Set expectations for the document's content
|
||||
|
||||
Keep the summary brief but comprehensive.
|
||||
"""
|
||||
|
||||
executiveSummary = await self.service.base.callAi([
|
||||
{"role": "system", "content": f"You are a documentation expert creating an executive summary in {formatType} format."},
|
||||
{"role": "user", "content": summaryPrompt}
|
||||
], produceUserAnswer = True)
|
||||
|
||||
# Step 2: Generate introduction
|
||||
introPrompt = f"""
|
||||
Create an introduction for a {documentType} titled "{title}".
|
||||
|
||||
DOCUMENT OVERVIEW:
|
||||
- Type: {documentType}
|
||||
- Audience: {audience}
|
||||
- Key Topics: {', '.join(keyTopics)}
|
||||
|
||||
TASK CONTEXT: {prompt}
|
||||
|
||||
The introduction should:
|
||||
1. Set the context and purpose of the document
|
||||
2. Outline the scope and objectives
|
||||
3. Preview the main topics to be covered
|
||||
4. Engage the reader's interest
|
||||
|
||||
Format the introduction according to {formatType} standards.
|
||||
"""
|
||||
|
||||
introduction = await self.service.base.callAi([
|
||||
{"role": "system", "content": f"You are a documentation expert creating an introduction in {formatType} format."},
|
||||
{"role": "user", "content": introPrompt}
|
||||
], produceUserAnswer = True)
|
||||
|
||||
# Step 3: Generate main sections
|
||||
sections = []
|
||||
for section in detailedStructure:
|
||||
sectionTitle = section.get("title", "Section")
|
||||
keyPoints = section.get("keyPoints", [])
|
||||
subsections = section.get("subsections", [])
|
||||
importance = section.get("importance", "medium")
|
||||
estimatedLength = section.get("estimatedLength", "medium")
|
||||
|
||||
sectionPrompt = f"""
|
||||
Create the {sectionTitle} section for a {documentType} titled "{title}".
|
||||
|
||||
SECTION DETAILS:
|
||||
- Title: {sectionTitle}
|
||||
- Key Points: {', '.join(keyPoints)}
|
||||
- Subsections: {', '.join(subsections)}
|
||||
- Importance: {importance}
|
||||
- Estimated Length: {estimatedLength}
|
||||
|
||||
DOCUMENT CONTEXT:
|
||||
- Type: {documentType}
|
||||
- Audience: {audience}
|
||||
- Key Topics: {', '.join(keyTopics)}
|
||||
|
||||
TASK CONTEXT: {prompt}
|
||||
|
||||
The section should:
|
||||
1. Cover all key points thoroughly
|
||||
2. Include relevant subsections
|
||||
3. Maintain appropriate depth based on importance
|
||||
4. Follow the document's tone and style
|
||||
|
||||
Format the section according to {formatType} standards.
|
||||
"""
|
||||
|
||||
sectionContent = await self.service.base.callAi([
|
||||
{"role": "system", "content": f"You are a documentation expert creating a section in {formatType} format."},
|
||||
{"role": "user", "content": sectionPrompt}
|
||||
], produceUserAnswer = True)
|
||||
|
||||
sections.append(sectionContent)
|
||||
|
||||
# Step 4: Generate conclusion
|
||||
conclusionPrompt = f"""
|
||||
Create the conclusion for a {documentType} titled "{title}".
|
||||
|
||||
DOCUMENT OVERVIEW:
|
||||
- Type: {documentType}
|
||||
- Audience: {audience}
|
||||
- Key Topics: {', '.join(keyTopics)}
|
||||
|
||||
TASK CONTEXT: {prompt}
|
||||
|
||||
This conclusion should:
|
||||
1. Summarize the key points covered in the document
|
||||
2. Provide closure to the topics discussed
|
||||
3. Include any relevant recommendations or next steps
|
||||
4. Leave the reader with a clear understanding of the document's significance
|
||||
|
||||
The conclusion should be professional and impactful, formatted according to {formatType} standards.
|
||||
"""
|
||||
|
||||
conclusion = await self.service.base.callAi([
|
||||
{"role": "system", "content": f"You are a documentation expert creating a conclusion in {formatType} format."},
|
||||
{"role": "user", "content": conclusionPrompt}
|
||||
], produceUserAnswer = True)
|
||||
|
||||
# Step 5: Assemble the complete document
|
||||
if formatType in ["md", "markdown"]:
|
||||
# Markdown format
|
||||
documentContent = f"# {title}\n\n"
|
||||
|
||||
if executiveSummary:
|
||||
documentContent += f"## Executive Summary\n\n{executiveSummary}\n\n"
|
||||
|
||||
documentContent += f"{introduction}\n\n"
|
||||
|
||||
for i, sectionContent in enumerate(sections):
|
||||
# Ensure section starts with heading if not already
|
||||
sectionTitle = detailedStructure[i].get("title", f"Section {i+1}")
|
||||
if not sectionContent.strip().startswith("#"):
|
||||
documentContent += f"## {sectionTitle}\n\n"
|
||||
documentContent += f"{sectionContent}\n\n"
|
||||
|
||||
documentContent += f"## Conclusion\n\n{conclusion}\n"
|
||||
|
||||
elif formatType == "html":
|
||||
# HTML format
|
||||
documentContent = f"<html>\n<head>\n<title>{title}</title>\n</head>\n<body>\n"
|
||||
documentContent += f"<h1>{title}</h1>\n\n"
|
||||
|
||||
if executiveSummary:
|
||||
documentContent += f"<h2>Executive Summary</h2>\n<div>{executiveSummary}</div>\n\n"
|
||||
|
||||
documentContent += f"<div>{introduction}</div>\n\n"
|
||||
|
||||
for i, sectionContent in enumerate(sections):
|
||||
sectionTitle = detailedStructure[i].get("title", f"Section {i+1}")
|
||||
documentContent += f"<h2>{sectionTitle}</h2>\n<div>{sectionContent}</div>\n\n"
|
||||
|
||||
documentContent += f"<h2>Conclusion</h2>\n<div>{conclusion}</div>\n"
|
||||
documentContent += "</body>\n</html>"
|
||||
|
||||
else:
|
||||
# Plain text format
|
||||
documentContent = f"{title}\n{'=' * len(title)}\n\n"
|
||||
|
||||
if executiveSummary:
|
||||
documentContent += f"EXECUTIVE SUMMARY\n{'-' * 17}\n\n{executiveSummary}\n\n"
|
||||
|
||||
documentContent += f"{introduction}\n\n"
|
||||
|
||||
for i, sectionContent in enumerate(sections):
|
||||
sectionTitle = detailedStructure[i].get("title", f"Section {i+1}")
|
||||
documentContent += f"{sectionTitle}\n{'-' * len(sectionTitle)}\n\n{sectionContent}\n\n"
|
||||
|
||||
documentContent += f"CONCLUSION\n{'-' * 10}\n\n{conclusion}\n"
|
||||
|
||||
# Create document object
|
||||
return self.formatAgentDocumentOutput(outputLabel, documentContent, contentType)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating document: {str(e)}", exc_info=True)
|
||||
|
||||
# Create a simple error document
|
||||
if formatType in ["md", "markdown"]:
|
||||
content = f"# Error in Documentation\n\nThere was an error generating the documentation: {str(e)}"
|
||||
elif formatType == "html":
|
||||
content = f"<html><body><h1>Error in Documentation</h1><p>There was an error generating the documentation: {str(e)}</p></body></html>"
|
||||
else:
|
||||
content = f"Error in Documentation\n\nThere was an error generating the documentation: {str(e)}"
|
||||
|
||||
return self.formatAgentDocumentOutput(outputLabel, content, contentType)
|
||||
|
||||
|
||||
# Factory function for the Documentation agent
|
||||
def getAgentDocumentation():
|
||||
"""Returns an instance of the Documentation agent."""
|
||||
return AgentDocumentation()
|
||||
380
modules/historic_data_agents/agentEmail.py
Normal file
380
modules/historic_data_agents/agentEmail.py
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
"""
|
||||
Email Agent Module.
|
||||
Handles email-related tasks using Microsoft Graph API.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import json
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
import uuid
|
||||
import os
|
||||
|
||||
from modules.workflow.agentBase import AgentBase
|
||||
from modules.interfaces.serviceChatModel import Task, ChatDocument, ChatContent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AgentEmail(AgentBase):
|
||||
"""Agent for handling email-related tasks."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the email agent."""
|
||||
super().__init__()
|
||||
self.name = "email"
|
||||
self.label = "Email Agent"
|
||||
self.description = "Handles email composition and sending using Microsoft Graph API"
|
||||
self.capabilities = [
|
||||
"email_composition",
|
||||
"email_draft_creation",
|
||||
"email_template_generation"
|
||||
]
|
||||
self.serviceBase = None
|
||||
|
||||
def setDependencies(self, serviceBase=None):
|
||||
"""Set external dependencies for the agent."""
|
||||
self.serviceBase = serviceBase
|
||||
|
||||
async def processTask(self, task: Task) -> Dict[str, Any]:
|
||||
"""
|
||||
Process an email-related task.
|
||||
|
||||
Args:
|
||||
task: Task object containing:
|
||||
- prompt: Instructions for the agent
|
||||
- inputDocuments: List of documents to process
|
||||
- outputSpecifications: List of required output documents
|
||||
- context: Additional context including workflow info
|
||||
|
||||
Returns:
|
||||
Dictionary containing:
|
||||
- feedback: Text response explaining what was done
|
||||
- documents: List of created documents
|
||||
"""
|
||||
try:
|
||||
# Extract task information
|
||||
prompt = task.prompt
|
||||
inputDocuments = task.filesInput
|
||||
outputSpecs = task.filesOutput
|
||||
|
||||
# Check AI service
|
||||
if not self.service.base:
|
||||
return {
|
||||
"feedback": "The Email agent requires an AI service to function.",
|
||||
"documents": []
|
||||
}
|
||||
|
||||
# Check if Microsoft connector is available
|
||||
if not hasattr(self.service, 'msft'):
|
||||
return {
|
||||
"feedback": "Microsoft connector not available. Please ensure Microsoft integration is properly configured.",
|
||||
"documents": []
|
||||
}
|
||||
|
||||
# Get Microsoft token
|
||||
token_data = self.service.msft.getMsftToken()
|
||||
if not token_data:
|
||||
# Create authentication trigger document
|
||||
auth_doc = self._createFrontendAuthTriggerDocument()
|
||||
return {
|
||||
"feedback": "Microsoft authentication required. Please authenticate to continue.",
|
||||
"documents": [auth_doc]
|
||||
}
|
||||
|
||||
# Extract document data from input
|
||||
documentContents, attachments = self._processInputDocuments(inputDocuments)
|
||||
|
||||
# Generate email subject and body using AI
|
||||
emailTemplate = await self._generateEmailTemplate(prompt, documentContents)
|
||||
|
||||
# Create HTML preview of the email
|
||||
htmlPreview = self._createHtmlPreview(emailTemplate)
|
||||
|
||||
# Attempt to create a draft email using Microsoft Graph API
|
||||
draft_result = self.service.msft.createDraftEmail(
|
||||
emailTemplate["recipient"],
|
||||
emailTemplate["subject"],
|
||||
emailTemplate["htmlBody"],
|
||||
attachments
|
||||
)
|
||||
|
||||
# Prepare output documents
|
||||
documents = []
|
||||
|
||||
# Process output specifications
|
||||
for spec in outputSpecs:
|
||||
label = spec.get("label", "")
|
||||
description = spec.get("description", "")
|
||||
|
||||
if label.endswith(".html"):
|
||||
# Create the HTML template file
|
||||
templateDoc = self.formatAgentDocumentOutput(
|
||||
label,
|
||||
emailTemplate["htmlBody"], # Use the actual HTML body, not the preview
|
||||
"text/html"
|
||||
)
|
||||
documents.append(templateDoc)
|
||||
elif label.endswith(".json"):
|
||||
# Create JSON template if requested
|
||||
templateJson = json.dumps(emailTemplate, indent=2)
|
||||
templateDoc = self.formatAgentDocumentOutput(
|
||||
label,
|
||||
templateJson,
|
||||
"application/json"
|
||||
)
|
||||
documents.append(templateDoc)
|
||||
else:
|
||||
# Default to preview for other cases
|
||||
previewDoc = self.formatAgentDocumentOutput(
|
||||
label,
|
||||
htmlPreview,
|
||||
"text/html"
|
||||
)
|
||||
documents.append(previewDoc)
|
||||
|
||||
# Prepare feedback message
|
||||
if draft_result:
|
||||
feedback = f"Email draft created successfully for {emailTemplate.get('recipient')}. The subject is: '{emailTemplate['subject']}'"
|
||||
if attachments:
|
||||
feedback += f" with {len(attachments)} attachment(s)"
|
||||
feedback += ". You can open and edit it in your Outlook draft folder."
|
||||
else:
|
||||
feedback = "Email template created but could not save as draft. HTML preview and template are available as documents."
|
||||
|
||||
return {
|
||||
"feedback": feedback,
|
||||
"documents": documents
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in email agent: {str(e)}")
|
||||
return {
|
||||
"feedback": f"Error processing email task: {str(e)}",
|
||||
"documents": []
|
||||
}
|
||||
|
||||
def _createFrontendAuthTriggerDocument(self) -> ChatDocument:
|
||||
"""Create a document that triggers Microsoft authentication in the frontend."""
|
||||
return ChatDocument(
|
||||
id=str(uuid.uuid4()),
|
||||
name="microsoft_auth",
|
||||
ext="html",
|
||||
data="""
|
||||
<div>
|
||||
<h2>Microsoft Authentication Required</h2>
|
||||
<p>Please click the button below to authenticate with Microsoft:</p>
|
||||
<button onclick="window.location.href='/api/auth/microsoft'">Authenticate with Microsoft</button>
|
||||
</div>
|
||||
""",
|
||||
contents=[
|
||||
ChatContent(
|
||||
name="main",
|
||||
data="""
|
||||
<div>
|
||||
<h2>Microsoft Authentication Required</h2>
|
||||
<p>Please click the button below to authenticate with Microsoft:</p>
|
||||
<button onclick="window.location.href='/api/auth/microsoft'">Authenticate with Microsoft</button>
|
||||
</div>
|
||||
""",
|
||||
summary="Microsoft authentication trigger page",
|
||||
metadata={
|
||||
"contentType": "text/html",
|
||||
"isText": True
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
def _processInputDocuments(self, input_docs: List[ChatDocument]) -> Tuple[str, List[Dict[str, Any]]]:
|
||||
"""
|
||||
Process input documents to extract content and prepare attachments.
|
||||
|
||||
Args:
|
||||
input_docs: List of input documents
|
||||
|
||||
Returns:
|
||||
Tuple of (document content text, list of attachments)
|
||||
"""
|
||||
documentContents = []
|
||||
attachments = []
|
||||
|
||||
for doc in input_docs:
|
||||
docName = doc.name
|
||||
if doc.ext:
|
||||
docName = f"{docName}.{doc.ext}"
|
||||
|
||||
# Add document name to contents
|
||||
documentContents.append(f"\n\n--- {docName} ---\n")
|
||||
|
||||
# Process document data directly
|
||||
if doc.data:
|
||||
# Add to attachments with proper metadata
|
||||
attachments.append({
|
||||
"name": docName,
|
||||
"document": {
|
||||
"data": doc.data,
|
||||
"mimeType": doc.contents[0].metadata.get("contentType", "application/octet-stream") if doc.contents else "application/octet-stream",
|
||||
"base64Encoded": doc.contents[0].metadata.get("base64Encoded", False) if doc.contents else False
|
||||
}
|
||||
})
|
||||
documentContents.append(f"Document attached: {docName}")
|
||||
else:
|
||||
documentContents.append(f"Document referenced: {docName}")
|
||||
|
||||
return "\n".join(documentContents), attachments
|
||||
|
||||
def formatAgentDocumentOutput(self, filename: str, content: str, contentType: str) -> ChatDocument:
|
||||
"""
|
||||
Format a document for agent output.
|
||||
|
||||
Args:
|
||||
filename: Output filename
|
||||
content: Document content
|
||||
contentType: MIME type of the content
|
||||
|
||||
Returns:
|
||||
ChatDocument object
|
||||
"""
|
||||
# Split filename into name and extension
|
||||
name, ext = os.path.splitext(filename)
|
||||
if ext.startswith('.'):
|
||||
ext = ext[1:]
|
||||
|
||||
# Create document object
|
||||
return ChatDocument(
|
||||
id=str(uuid.uuid4()),
|
||||
name=name,
|
||||
ext=ext,
|
||||
data=content,
|
||||
contents=[
|
||||
ChatContent(
|
||||
name="main",
|
||||
data=content,
|
||||
summary=f"Generated {filename}",
|
||||
metadata={"contentType": contentType}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
async def _generateEmailTemplate(self, prompt: str, documentContents: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate email template using AI.
|
||||
|
||||
Args:
|
||||
prompt: The task prompt
|
||||
documentContents: Extracted document content
|
||||
|
||||
Returns:
|
||||
Email template dictionary with recipient, subject, body
|
||||
"""
|
||||
emailPrompt = f"""
|
||||
Create an email based on the following request:
|
||||
|
||||
REQUEST: {prompt}
|
||||
|
||||
DOCUMENT CONTENTS:
|
||||
{documentContents[:2000]}... (truncated if longer)
|
||||
|
||||
Generate an email template with:
|
||||
1. A relevant recipient (use placeholder or derive from content if possible)
|
||||
2. A concise but descriptive subject line
|
||||
3. A professional HTML-formatted email body
|
||||
4. Appropriate greeting and closing
|
||||
|
||||
Format your response as JSON with these fields:
|
||||
- recipient: email address
|
||||
- subject: subject line
|
||||
- plainBody: plain text version
|
||||
- htmlBody: HTML formatted version
|
||||
|
||||
Only return valid JSON. No preamble or explanations.
|
||||
"""
|
||||
|
||||
try:
|
||||
response = await self.service.base.callAi([
|
||||
{"role": "system", "content": "You are an email template specialist. Create professional emails. Respond with valid JSON only."},
|
||||
{"role": "user", "content": emailPrompt}
|
||||
])
|
||||
|
||||
# Extract JSON from response
|
||||
jsonStart = response.find('{')
|
||||
jsonEnd = response.rfind('}') + 1
|
||||
|
||||
if jsonStart >= 0 and jsonEnd > jsonStart:
|
||||
template = json.loads(response[jsonStart:jsonEnd])
|
||||
return template
|
||||
else:
|
||||
# Fallback plan
|
||||
logger.warning(f"Not able creating email template, generating fallback plan")
|
||||
return {
|
||||
"recipient": "recipient@example.com",
|
||||
"subject": "Information Regarding Your Request",
|
||||
"plainBody": f"This email is regarding your request: {prompt}",
|
||||
"htmlBody": f"<html><body><p>This email is regarding your request: {prompt}</p></body></html>"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error generating email template: {str(e)}")
|
||||
return {
|
||||
"recipient": "recipient@example.com",
|
||||
"subject": "Information Regarding Your Request",
|
||||
"plainBody": f"This email is regarding your request: {prompt}",
|
||||
"htmlBody": f"<html><body><p>This email is regarding your request: {prompt}</p></body></html>"
|
||||
}
|
||||
|
||||
def _createHtmlPreview(self, emailTemplate: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Create an HTML preview of the email template.
|
||||
|
||||
Args:
|
||||
emailTemplate: Email template dictionary
|
||||
|
||||
Returns:
|
||||
HTML string for preview
|
||||
"""
|
||||
html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Email Preview: {emailTemplate.get('subject', 'Email Template')}</title>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }}
|
||||
.email-container {{ max-width: 600px; margin: 20px auto; background-color: white; border: 1px solid #ddd; border-radius: 5px; overflow: hidden; }}
|
||||
.email-header {{ background-color: #f0f0f0; padding: 15px; border-bottom: 1px solid #ddd; }}
|
||||
.email-content {{ padding: 20px; }}
|
||||
.email-footer {{ background-color: #f0f0f0; padding: 15px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }}
|
||||
.field {{ margin-bottom: 10px; }}
|
||||
.field-label {{ font-weight: bold; color: #555; }}
|
||||
.email-body {{ margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h2>Email Template Preview</h2>
|
||||
</div>
|
||||
<div class="email-content">
|
||||
<div class="field">
|
||||
<div class="field-label">To:</div>
|
||||
<div>{emailTemplate.get('recipient', 'recipient@example.com')}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Subject:</div>
|
||||
<div>{emailTemplate.get('subject', 'No Subject')}</div>
|
||||
</div>
|
||||
<div class="email-body">
|
||||
{emailTemplate.get('htmlBody', '<p>No content</p>')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
<p>This is a preview of the email template. The actual email may appear differently in various email clients.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return html
|
||||
|
||||
def getAgentEmail() -> AgentEmail:
|
||||
"""Factory function to create and return an EmailAgent instance."""
|
||||
return AgentEmail()
|
||||
348
modules/historic_data_agents/agentSharepoint.py
Normal file
348
modules/historic_data_agents/agentSharepoint.py
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
"""
|
||||
SharePoint Agent Module.
|
||||
Handles SharePoint document search and data extraction using Microsoft Graph API.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import json
|
||||
from typing import Dict, Any, List, Optional
|
||||
from modules.workflow.agentBase import AgentBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AgentSharepoint(AgentBase):
|
||||
"""Agent for handling SharePoint document operations."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the SharePoint agent."""
|
||||
super().__init__()
|
||||
self.name = "sharepoint"
|
||||
self.label = "SharePoint Agent"
|
||||
self.description = "Searches and extracts data from SharePoint documents using Microsoft Graph API"
|
||||
self.capabilities = [
|
||||
"document_search",
|
||||
"content_extraction",
|
||||
"metadata_analysis",
|
||||
"document_processing"
|
||||
]
|
||||
|
||||
async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Process a SharePoint-related task.
|
||||
|
||||
Args:
|
||||
task: Task object containing:
|
||||
- prompt: Instructions for the agent
|
||||
- inputDocuments: List of documents to process
|
||||
- outputSpecifications: List of required output documents
|
||||
- context: Additional context including workflow info
|
||||
|
||||
Returns:
|
||||
Dictionary containing:
|
||||
- feedback: Text response explaining what was done
|
||||
- documents: List of created documents
|
||||
"""
|
||||
try:
|
||||
# Extract task information
|
||||
prompt = task.get("prompt", "")
|
||||
inputDocuments = task.get("inputDocuments", [])
|
||||
outputSpecs = task.get("outputSpecifications", [])
|
||||
|
||||
# Check AI service
|
||||
if not self.service.base:
|
||||
return {
|
||||
"feedback": "The SharePoint agent requires an AI service to function.",
|
||||
"documents": []
|
||||
}
|
||||
|
||||
# Check if Microsoft connector is available
|
||||
if not hasattr(self.service, 'msft'):
|
||||
return {
|
||||
"feedback": "Microsoft connector not available. Please ensure Microsoft integration is properly configured.",
|
||||
"documents": []
|
||||
}
|
||||
|
||||
# Get Microsoft token
|
||||
token_data = self.service.msft.getMsftToken()
|
||||
if not token_data:
|
||||
# Create authentication trigger document
|
||||
auth_doc = self._createFrontendAuthTriggerDocument()
|
||||
return {
|
||||
"feedback": "Microsoft authentication required. Please authenticate to continue.",
|
||||
"documents": [auth_doc]
|
||||
}
|
||||
|
||||
# Parse the search query from the prompt
|
||||
searchQuery = await self._parseSearchQuery(prompt)
|
||||
|
||||
# Search SharePoint documents
|
||||
searchResults = await self._searchSharePointDocuments(searchQuery)
|
||||
|
||||
# Process search results
|
||||
documents = []
|
||||
for spec in outputSpecs:
|
||||
label = spec.get("label", "")
|
||||
description = spec.get("description", "")
|
||||
|
||||
if label.endswith(".json"):
|
||||
# Create JSON summary of search results
|
||||
summaryDoc = self._createSearchSummaryJson(searchResults, description)
|
||||
documents.append(summaryDoc)
|
||||
elif label.endswith(".csv"):
|
||||
# Create CSV summary of search results
|
||||
summaryDoc = self._createSearchSummaryCsv(searchResults, description)
|
||||
documents.append(summaryDoc)
|
||||
else:
|
||||
# Create text summary of search results
|
||||
summaryDoc = self._createSearchSummaryText(searchResults, description)
|
||||
documents.append(summaryDoc)
|
||||
|
||||
# Prepare feedback message
|
||||
feedback = f"Found {len(searchResults)} documents matching your search criteria. "
|
||||
if searchResults:
|
||||
feedback += "The results have been saved as documents."
|
||||
else:
|
||||
feedback += "No matching documents were found."
|
||||
|
||||
return {
|
||||
"feedback": feedback,
|
||||
"documents": documents
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in SharePoint agent: {str(e)}")
|
||||
return {
|
||||
"feedback": f"Error processing SharePoint task: {str(e)}",
|
||||
"documents": []
|
||||
}
|
||||
|
||||
def _createFrontendAuthTriggerDocument(self) -> Dict[str, Any]:
|
||||
"""Create a document that triggers Microsoft authentication in the frontend."""
|
||||
return self.formatAgentDocumentOutput(
|
||||
"microsoft_auth.html",
|
||||
"""
|
||||
<div>
|
||||
<h2>Microsoft Authentication Required</h2>
|
||||
<p>Please click the button below to authenticate with Microsoft:</p>
|
||||
<button onclick="window.location.href='/api/auth/microsoft'">Authenticate with Microsoft</button>
|
||||
</div>
|
||||
""",
|
||||
"text/html"
|
||||
)
|
||||
|
||||
async def _parseSearchQuery(self, prompt: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse the search query from the prompt using AI.
|
||||
|
||||
Args:
|
||||
prompt: The task prompt
|
||||
|
||||
Returns:
|
||||
Dictionary containing search parameters
|
||||
"""
|
||||
try:
|
||||
# Use AI to parse the search query
|
||||
response = await self.service.base.callAi([
|
||||
{"role": "system", "content": "You are a SharePoint search query parser. Extract search parameters from the user's request."},
|
||||
{"role": "user", "content": f"""
|
||||
Parse the following SharePoint search request into structured parameters:
|
||||
|
||||
{prompt}
|
||||
|
||||
Return a JSON object with these fields:
|
||||
- query: The main search query
|
||||
- site: Optional SharePoint site name
|
||||
- folder: Optional folder path
|
||||
- fileTypes: List of file types to search for
|
||||
- dateRange: Optional date range for filtering
|
||||
- maxResults: Maximum number of results to return
|
||||
|
||||
Only return valid JSON. No preamble or explanations.
|
||||
"""}
|
||||
])
|
||||
|
||||
# Extract JSON from response
|
||||
jsonStart = response.find('{')
|
||||
jsonEnd = response.rfind('}') + 1
|
||||
|
||||
if jsonStart >= 0 and jsonEnd > jsonStart:
|
||||
return json.loads(response[jsonStart:jsonEnd])
|
||||
else:
|
||||
# Fallback to simple query
|
||||
return {
|
||||
"query": prompt,
|
||||
"maxResults": 10
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error parsing search query: {str(e)}")
|
||||
return {
|
||||
"query": prompt,
|
||||
"maxResults": 10
|
||||
}
|
||||
|
||||
async def _searchSharePointDocuments(self, searchParams: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Search SharePoint documents using Microsoft Graph API.
|
||||
|
||||
Args:
|
||||
searchParams: Search parameters
|
||||
|
||||
Returns:
|
||||
List of search results
|
||||
"""
|
||||
try:
|
||||
# Get Microsoft token
|
||||
token = self.service.msft.getMsftToken()
|
||||
if not token:
|
||||
return []
|
||||
|
||||
# Prepare search query
|
||||
query = searchParams.get("query", "")
|
||||
site = searchParams.get("site", "")
|
||||
folder = searchParams.get("folder", "")
|
||||
fileTypes = searchParams.get("fileTypes", [])
|
||||
maxResults = searchParams.get("maxResults", 10)
|
||||
|
||||
# Build search URL
|
||||
searchUrl = "https://graph.microsoft.com/v1.0/sites/root/drives"
|
||||
if site:
|
||||
searchUrl = f"https://graph.microsoft.com/v1.0/sites/{site}/drives"
|
||||
|
||||
# Get drives (document libraries)
|
||||
response = self.service.msft.makeGraphRequest("GET", searchUrl)
|
||||
if not response or "value" not in response:
|
||||
return []
|
||||
|
||||
results = []
|
||||
for drive in response["value"]:
|
||||
# Search in each drive
|
||||
driveId = drive["id"]
|
||||
searchEndpoint = f"https://graph.microsoft.com/v1.0/drives/{driveId}/root/search(q='{query}')"
|
||||
|
||||
# Add file type filters if specified
|
||||
if fileTypes:
|
||||
typeFilter = " or ".join([f"fileType eq '{ft}'" for ft in fileTypes])
|
||||
searchEndpoint += f"&filter={typeFilter}"
|
||||
|
||||
# Add folder filter if specified
|
||||
if folder:
|
||||
searchEndpoint += f"&filter=parentReference/path eq '/{folder}'"
|
||||
|
||||
# Add result limit
|
||||
searchEndpoint += f"&top={maxResults}"
|
||||
|
||||
# Make the search request
|
||||
searchResponse = self.service.msft.makeGraphRequest("GET", searchEndpoint)
|
||||
if searchResponse and "value" in searchResponse:
|
||||
for item in searchResponse["value"]:
|
||||
# Get file content
|
||||
fileContent = await self._getFileContent(driveId, item["id"])
|
||||
|
||||
results.append({
|
||||
"name": item["name"],
|
||||
"id": item["id"],
|
||||
"driveId": driveId,
|
||||
"webUrl": item["webUrl"],
|
||||
"lastModified": item["lastModifiedDateTime"],
|
||||
"size": item["size"],
|
||||
"content": fileContent
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching SharePoint: {str(e)}")
|
||||
return []
|
||||
|
||||
async def _getFileContent(self, driveId: str, fileId: str) -> str:
|
||||
"""
|
||||
Get file content from SharePoint.
|
||||
|
||||
Args:
|
||||
driveId: Drive ID
|
||||
fileId: File ID
|
||||
|
||||
Returns:
|
||||
File content as string
|
||||
"""
|
||||
try:
|
||||
# Get file content URL
|
||||
contentUrl = f"https://graph.microsoft.com/v1.0/drives/{driveId}/items/{fileId}/content"
|
||||
|
||||
# Download file content
|
||||
response = self.service.msft.makeGraphRequest("GET", contentUrl, raw=True)
|
||||
if response:
|
||||
return response.decode('utf-8')
|
||||
return ""
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting file content: {str(e)}")
|
||||
return ""
|
||||
|
||||
def _createSearchSummaryJson(self, results: List[Dict[str, Any]], description: str) -> Dict[str, Any]:
|
||||
"""Create a JSON summary of search results."""
|
||||
summary = {
|
||||
"description": description,
|
||||
"totalResults": len(results),
|
||||
"results": []
|
||||
}
|
||||
|
||||
for result in results:
|
||||
summary["results"].append({
|
||||
"name": result["name"],
|
||||
"url": result["webUrl"],
|
||||
"lastModified": result["lastModified"],
|
||||
"size": result["size"]
|
||||
})
|
||||
|
||||
return self.formatAgentDocumentOutput(
|
||||
"sharepoint_search_results.json",
|
||||
json.dumps(summary, indent=2),
|
||||
"application/json"
|
||||
)
|
||||
|
||||
def _createSearchSummaryCsv(self, results: List[Dict[str, Any]], description: str) -> Dict[str, Any]:
|
||||
"""Create a CSV summary of search results."""
|
||||
csvLines = ["Name,URL,Last Modified,Size (bytes)"]
|
||||
|
||||
for result in results:
|
||||
name = result["name"].replace('"', '""')
|
||||
url = result["webUrl"].replace('"', '""')
|
||||
lastModified = result["lastModified"].replace('"', '""')
|
||||
size = str(result["size"])
|
||||
|
||||
csvLines.append(f'"{name}","{url}","{lastModified}",{size}')
|
||||
|
||||
return self.formatAgentDocumentOutput(
|
||||
"sharepoint_search_results.csv",
|
||||
"\n".join(csvLines),
|
||||
"text/csv"
|
||||
)
|
||||
|
||||
def _createSearchSummaryText(self, results: List[Dict[str, Any]], description: str) -> Dict[str, Any]:
|
||||
"""Create a text summary of search results."""
|
||||
textLines = [
|
||||
f"SharePoint Search Results",
|
||||
f"Description: {description}",
|
||||
f"Total Results: {len(results)}",
|
||||
"\nResults:"
|
||||
]
|
||||
|
||||
for result in results:
|
||||
textLines.extend([
|
||||
f"\nName: {result['name']}",
|
||||
f"URL: {result['webUrl']}",
|
||||
f"Last Modified: {result['lastModified']}",
|
||||
f"Size: {result['size']} bytes"
|
||||
])
|
||||
|
||||
return self.formatAgentDocumentOutput(
|
||||
"sharepoint_search_results.txt",
|
||||
"\n".join(textLines),
|
||||
"text/plain"
|
||||
)
|
||||
|
||||
def getAgentSharepoint() -> AgentSharepoint:
|
||||
"""Factory function to create and return a SharePointAgent instance."""
|
||||
return AgentSharepoint()
|
||||
814
modules/historic_data_agents/agentWebcrawler.py
Normal file
814
modules/historic_data_agents/agentWebcrawler.py
Normal file
|
|
@ -0,0 +1,814 @@
|
|||
"""
|
||||
Web crawler agent for gathering and analyzing web content.
|
||||
Provides web research and content extraction capabilities.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import os
|
||||
from typing import Dict, Any, List
|
||||
from urllib.parse import quote_plus, unquote
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
import requests
|
||||
import markdown
|
||||
|
||||
from modules.workflow.agentBase import AgentBase
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AgentWebcrawler(AgentBase):
|
||||
"""AI-driven agent for web research and information retrieval"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the web crawler agent"""
|
||||
super().__init__()
|
||||
self.name = "webcrawler"
|
||||
self.label = "Web Crawler"
|
||||
self.description = "Gathers and analyzes web content using AI with multi-step research"
|
||||
self.capabilities = [
|
||||
"web_research",
|
||||
"content_gathering",
|
||||
"data_extraction",
|
||||
"information_synthesis",
|
||||
"source_verification"
|
||||
]
|
||||
|
||||
# Web crawling configuration
|
||||
self.srcApikey = APP_CONFIG.get("Agent_Webcrawler_SERPAPI_APIKEY","")
|
||||
self.srcEngine = APP_CONFIG.get("Agent_Webcrawler_SERPAPI_ENGINE","google")
|
||||
self.srcCountry = APP_CONFIG.get("Agent_Webcrawler_SERPAPI_COUNTRY","auto")
|
||||
self.maxUrl = int(APP_CONFIG.get("Agent_Webcrawler_SERPAPI_MAX_URLS", "5"))
|
||||
self.maxSearchTerms = int(APP_CONFIG.get("Agent_Webcrawler_SERPAPI_MAX_SEARCH_KEYWORDS", "3"))
|
||||
self.maxResults = int(APP_CONFIG.get("Agent_Webcrawler_SERPAPI_MAX_SEARCH_RESULTS", "5"))
|
||||
self.timeout = int(APP_CONFIG.get("Agent_Webcrawler_SERPAPI_TIMEOUT", "30"))
|
||||
self.userAgent = APP_CONFIG.get("Agent_Webcrawler_SERPAPI_USER_AGENT", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
|
||||
if not self.srcApikey:
|
||||
logger.error("SerpAPI key not configured")
|
||||
|
||||
def setDependencies(self, serviceBase=None):
|
||||
"""Set external dependencies for the agent."""
|
||||
self.setService(serviceBase)
|
||||
|
||||
async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Process a task by focusing on required outputs and using AI to guide the research process.
|
||||
|
||||
Args:
|
||||
task: Task dictionary with prompt, inputDocuments, outputSpecifications
|
||||
|
||||
Returns:
|
||||
Dictionary with feedback and documents
|
||||
"""
|
||||
try:
|
||||
# Extract task information
|
||||
prompt = task.get("prompt", "")
|
||||
inputDocuments = task.get("inputDocuments", [])
|
||||
outputSpecs = task.get("outputSpecifications", [])
|
||||
workflow = task.get("context", {}).get("workflow", {})
|
||||
|
||||
# Check AI service
|
||||
if not self.service or not self.service.base:
|
||||
return {
|
||||
"feedback": "The Web Crawler agent requires an AI service to function.",
|
||||
"documents": []
|
||||
}
|
||||
|
||||
# Create research plan
|
||||
if workflow:
|
||||
self.service.logAdd(workflow, "Creating research plan...", level="info", progress=35)
|
||||
researchPlan = await self._createResearchPlan(prompt)
|
||||
|
||||
# Check if this is truly a web research task
|
||||
if not researchPlan.get("requiresWebResearch", True):
|
||||
return {
|
||||
"feedback": "This task doesn't appear to require web research. Please try a different agent.",
|
||||
"documents": []
|
||||
}
|
||||
|
||||
# Gather raw material through web research
|
||||
if workflow:
|
||||
self.service.logAdd(workflow, "Gathering research material...", level="info", progress=45)
|
||||
rawResults = await self._gatherResearchMaterial(researchPlan, workflow)
|
||||
|
||||
# Format results into requested output documents
|
||||
if workflow:
|
||||
self.service.logAdd(workflow, "Creating output documents...", level="info", progress=55)
|
||||
documents = await self._createOutputDocuments(
|
||||
prompt,
|
||||
rawResults,
|
||||
outputSpecs,
|
||||
researchPlan
|
||||
)
|
||||
|
||||
# Generate feedback
|
||||
feedback = researchPlan.get("feedback", f"I conducted web research on '{prompt[:50]}...' and gathered information from {len(rawResults)} relevant sources.")
|
||||
|
||||
return {
|
||||
"feedback": feedback,
|
||||
"documents": documents
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during web research: {str(e)}", exc_info=True)
|
||||
return {
|
||||
"feedback": f"Error during web research: {str(e)}",
|
||||
"documents": []
|
||||
}
|
||||
|
||||
async def _createResearchPlan(self, prompt: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Use AI to create a detailed research plan.
|
||||
|
||||
Args:
|
||||
prompt: The research query
|
||||
|
||||
Returns:
|
||||
Research plan dictionary
|
||||
"""
|
||||
researchPrompt = f"""
|
||||
Create a detailed web research plan for this task: "{prompt}"
|
||||
|
||||
Analyze the request carefully and create a structured plan in JSON format with the following elements:
|
||||
{{
|
||||
"requiresWebResearch": true/false, # Whether this genuinely requires web research
|
||||
"researchQuestions": ["question1", "question2", ...], # 2-4 specific questions to answer
|
||||
"searchTerms": ["term1", "term2", ...], # Up to {self.maxSearchTerms} effective search terms
|
||||
"directUrls": ["url1", "url2", ...], # Any URLs directly mentioned in the request (up to {self.maxUrl})
|
||||
"expectedSources": ["type1", "type2", ...], # Types of sources that would be most valuable
|
||||
"contentFocus": "what specific content to extract or focus on",
|
||||
"feedback": "explanation of how the research will be conducted"
|
||||
}}
|
||||
|
||||
Respond with ONLY the JSON object, no additional text or explanations.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Get research plan from AI
|
||||
response = await self.service.base.callAi([
|
||||
{"role": "system", "content": "You are a research expert. Respond with valid JSON only."},
|
||||
{"role": "user", "content": researchPrompt}
|
||||
])
|
||||
|
||||
# Extract JSON
|
||||
jsonStart = response.find('{')
|
||||
jsonEnd = response.rfind('}') + 1
|
||||
|
||||
if jsonStart >= 0 and jsonEnd > jsonStart:
|
||||
plan = json.loads(response[jsonStart:jsonEnd])
|
||||
|
||||
# Ensure we have the expected fields with defaults if missing
|
||||
if "searchTerms" not in plan:
|
||||
plan["searchTerms"] = [prompt]
|
||||
if "directUrls" not in plan:
|
||||
plan["directUrls"] = []
|
||||
if "researchQuestions" not in plan:
|
||||
plan["researchQuestions"] = ["What information can be found about this topic?"]
|
||||
|
||||
return plan
|
||||
else:
|
||||
# Fallback plan
|
||||
logger.warning(f"Not able creating research plan, generating fallback plan")
|
||||
return {
|
||||
"requiresWebResearch": True,
|
||||
"researchQuestions": ["What information can be found about this topic?"],
|
||||
"searchTerms": [prompt],
|
||||
"directUrls": [],
|
||||
"expectedSources": ["Web pages", "Articles"],
|
||||
"contentFocus": "Relevant information about the topic",
|
||||
"feedback": f"I'll conduct web research on '{prompt}' and gather relevant information."
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error creating research plan: {str(e)}")
|
||||
# Simple fallback plan
|
||||
return {
|
||||
"requiresWebResearch": True,
|
||||
"researchQuestions": ["What information can be found about this topic?"],
|
||||
"searchTerms": [prompt],
|
||||
"directUrls": [],
|
||||
"expectedSources": ["Web pages", "Articles"],
|
||||
"contentFocus": "Relevant information about the topic",
|
||||
"feedback": f"I'll conduct web research on '{prompt}' and gather relevant information."
|
||||
}
|
||||
|
||||
async def _gatherResearchMaterial(self, researchPlan: Dict[str, Any], workflow: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Gather research material based on the research plan.
|
||||
|
||||
Args:
|
||||
researchPlan: Research plan dictionary
|
||||
workflow: Current workflow object
|
||||
|
||||
Returns:
|
||||
List of research results
|
||||
"""
|
||||
allResults = []
|
||||
|
||||
# Process direct URLs
|
||||
directUrls = researchPlan.get("directUrls", [])[:self.maxUrl]
|
||||
for i, url in enumerate(directUrls):
|
||||
progress = 45 + int((i / len(directUrls)) * 5) # Progress from 45% to 50%
|
||||
self.service.logAdd(workflow, f"Processing direct URL {i+1}/{len(directUrls)}...", level="info", progress=progress)
|
||||
logger.info(f"Processing direct URL: {url}")
|
||||
try:
|
||||
# Fetch and extract content
|
||||
soup = self._readUrl(url)
|
||||
|
||||
if soup:
|
||||
# Extract title and content
|
||||
title = self._extractTitle(soup, url)
|
||||
content = self._extractMainContent(soup)
|
||||
|
||||
# Add to results
|
||||
allResults.append({
|
||||
"title": title,
|
||||
"url": url,
|
||||
"sourceType": "directUrl",
|
||||
"content": content,
|
||||
"summary": "" # Will be filled later
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f"Error processing URL {url}: {str(e)}")
|
||||
|
||||
# Process search terms
|
||||
searchTerms = researchPlan.get("searchTerms", [])[:self.maxSearchTerms]
|
||||
for i, term in enumerate(searchTerms):
|
||||
progress = 50 + int((i / len(searchTerms)) * 5) # Progress from 50% to 55%
|
||||
self.service.logAdd(workflow, f"Searching term {i+1}/{len(searchTerms)}...", level="info", progress=progress)
|
||||
logger.info(f"Searching for: {term}")
|
||||
try:
|
||||
# Perform search
|
||||
searchResults = self._searchWeb(term)
|
||||
|
||||
# Process each search result
|
||||
for result in searchResults:
|
||||
# Check if URL is already in results
|
||||
if not any(r["url"] == result["url"] for r in allResults):
|
||||
allResults.append({
|
||||
"title": result["title"],
|
||||
"url": result["url"],
|
||||
"sourceType": "searchResult",
|
||||
"content": result["data"],
|
||||
"snippet": result["snippet"],
|
||||
"summary": "" # Will be filled later
|
||||
})
|
||||
|
||||
# Stop if we've reached the maximum results
|
||||
if len(allResults) >= self.maxResults:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(f"Error searching for {term}: {str(e)}")
|
||||
|
||||
# Stop if we've reached the maximum results
|
||||
if len(allResults) >= self.maxResults:
|
||||
break
|
||||
|
||||
# Create summaries for all results
|
||||
allResults = await self._summarizeAllResults(allResults, researchPlan)
|
||||
|
||||
return allResults
|
||||
|
||||
async def _summarizeAllResults(self, results: List[Dict[str, Any]], researchPlan: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Create summaries for all research results.
|
||||
|
||||
Args:
|
||||
results: List of research results
|
||||
researchPlan: Research plan with questions and focus
|
||||
|
||||
Returns:
|
||||
Results with added summaries
|
||||
"""
|
||||
for i, result in enumerate(results):
|
||||
logger.info(f"Summarizing result {i+1}/{len(results)}: {result['title'][:30]}...")
|
||||
|
||||
try:
|
||||
# Limit content length to avoid token issues
|
||||
content = self._limitText(result.get("content", ""), maxChars=8000)
|
||||
researchQuestions = researchPlan.get("researchQuestions", ["What relevant information does this page contain?"])
|
||||
contentFocus = researchPlan.get("contentFocus", "Relevant information")
|
||||
|
||||
# Create summary using AI
|
||||
summaryPrompt = f"""
|
||||
Summarize this web page content based on these research questions:
|
||||
{', '.join(researchQuestions)}
|
||||
|
||||
Focus on: {contentFocus}
|
||||
|
||||
Web page: {result['url']}
|
||||
Title: {result['title']}
|
||||
|
||||
Content:
|
||||
{content}
|
||||
|
||||
Create a concise summary that:
|
||||
1. Directly answers the research questions if possible
|
||||
2. Extracts the most relevant information from the page
|
||||
3. Includes specific facts, figures, or quotes if available
|
||||
4. Is around 2000 characters long
|
||||
|
||||
Only include information actually found in the content. No fabrications or assumptions.
|
||||
"""
|
||||
|
||||
# Get summary from AI
|
||||
summary = await self.service.base.callAi([
|
||||
{"role": "system", "content": "You are a research expert. Respond with valid JSON only."},
|
||||
{"role": "user", "content": summaryPrompt}
|
||||
])
|
||||
|
||||
# Add summary to result
|
||||
result["summary"] = summary.strip()
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error summarizing result {i+1}: {str(e)}")
|
||||
result["summary"] = f"Error creating summary: {str(e)}"
|
||||
|
||||
return results
|
||||
|
||||
async def _createOutputDocuments(self, prompt: str, results: List[Dict[str, Any]],
|
||||
outputSpecs: List[Dict[str, Any]], researchPlan: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Create output documents based on research results and specifications.
|
||||
|
||||
Args:
|
||||
prompt: Original research prompt
|
||||
results: List of research results
|
||||
outputSpecs: Output specifications
|
||||
researchPlan: Research plan
|
||||
|
||||
Returns:
|
||||
List of output documents
|
||||
"""
|
||||
# If no output specs provided, create default output
|
||||
if not outputSpecs:
|
||||
outputSpecs = [{
|
||||
"label": "webResearchResults.md",
|
||||
"description": "Comprehensive web research results"
|
||||
}]
|
||||
|
||||
# Generate documents
|
||||
documents = []
|
||||
|
||||
# Process each output specification
|
||||
for spec in outputSpecs:
|
||||
outputLabel = spec.get("label", "")
|
||||
outputDescription = spec.get("description", "")
|
||||
|
||||
# Determine format based on file extension
|
||||
formatType = self._determineFormatType(outputLabel)
|
||||
|
||||
# Create appropriate document based on format
|
||||
if formatType == "json":
|
||||
# JSON output - structured data
|
||||
document = await self._createJsonDocument(prompt, results, researchPlan, outputLabel)
|
||||
elif formatType == "csv":
|
||||
# CSV output - tabular data
|
||||
document = await self._createCsvDocument(results, outputLabel)
|
||||
else:
|
||||
# Text-based output (markdown, html, text) - narrative report
|
||||
document = await self._createNarrativeDocument(
|
||||
prompt, results, researchPlan, formatType, outputLabel, outputDescription
|
||||
)
|
||||
|
||||
documents.append(document)
|
||||
|
||||
return documents
|
||||
|
||||
async def _createNarrativeDocument(self, prompt: str, results: List[Dict[str, Any]],
|
||||
researchPlan: Dict[str, Any], formatType: str,
|
||||
outputLabel: str, outputDescription: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a narrative document (markdown, html, text) from research results.
|
||||
|
||||
Args:
|
||||
prompt: Original research prompt
|
||||
results: Research results
|
||||
researchPlan: Research plan
|
||||
formatType: Output format (markdown, html, text)
|
||||
outputLabel: Output filename
|
||||
outputDescription: Output description
|
||||
|
||||
Returns:
|
||||
Document object
|
||||
"""
|
||||
# Create content based on format
|
||||
if formatType == "markdown":
|
||||
contentType = "text/markdown"
|
||||
templateFormat = "markdown"
|
||||
elif formatType == "html":
|
||||
contentType = "text/html"
|
||||
templateFormat = "html"
|
||||
else:
|
||||
contentType = "text/plain"
|
||||
templateFormat = "text"
|
||||
|
||||
# Prepare research context
|
||||
researchQuestions = researchPlan.get("researchQuestions", [])
|
||||
searchTerms = researchPlan.get("searchTerms", [])
|
||||
|
||||
# Create document structure based on results
|
||||
sourcesSummary = []
|
||||
for result in results:
|
||||
sourcesSummary.append({
|
||||
"title": result.get("title", "Untitled"),
|
||||
"url": result.get("url", ""),
|
||||
"summary": result.get("summary", ""),
|
||||
"snippet": result.get("snippet", "")
|
||||
})
|
||||
|
||||
# Truncate content for prompt
|
||||
sourcesJson = json.dumps(sourcesSummary, indent=2)
|
||||
if len(sourcesJson) > 10000:
|
||||
# Logic to truncate each summary while preserving structure
|
||||
for i in range(len(sourcesSummary)):
|
||||
if len(sourcesJson) <= 10000:
|
||||
break
|
||||
# Gradually truncate summaries
|
||||
sourcesSummary[i]["summary"] = sourcesSummary[i]["summary"][:500] + "..."
|
||||
sourcesJson = json.dumps(sourcesSummary, indent=2)
|
||||
|
||||
# Create report prompt
|
||||
reportPrompt = f"""
|
||||
Create a comprehensive {formatType} research report based on the following web research:
|
||||
|
||||
TASK: {prompt}
|
||||
|
||||
RESEARCH QUESTIONS:
|
||||
{', '.join(researchQuestions)}
|
||||
|
||||
SEARCH TERMS USED:
|
||||
{', '.join(searchTerms)}
|
||||
|
||||
SOURCES AND FINDINGS:
|
||||
{sourcesJson}
|
||||
|
||||
REPORT DETAILS:
|
||||
- Format: {templateFormat}
|
||||
- Filename: {outputLabel}
|
||||
- Description: {outputDescription}
|
||||
|
||||
Create a well-structured report that:
|
||||
1. Includes an executive summary of key findings
|
||||
2. Addresses each research question directly
|
||||
3. Integrates information from all relevant sources
|
||||
4. Cites sources appropriately for each piece of information
|
||||
5. Provides a comprehensive synthesis of the research
|
||||
6. Is formatted professionally and appropriately for {templateFormat}
|
||||
|
||||
The report should be scholarly, accurate, and focused on the original research task.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Generate report with AI
|
||||
reportContent = await self.service.base.callAi([
|
||||
{"role": "system", "content": "You are a research expert. Respond with valid JSON only."},
|
||||
{"role": "user", "content": reportPrompt}
|
||||
])
|
||||
|
||||
# Convert to HTML if needed
|
||||
if formatType == "html" and not reportContent.lower().startswith("<html"):
|
||||
# Check if it's markdown that needs conversion
|
||||
if reportContent.startswith("#"):
|
||||
reportContent = markdown.markdown(reportContent)
|
||||
# Wrap in basic HTML structure if needed
|
||||
if not reportContent.lower().startswith("<html"):
|
||||
reportContent = f"<html><head><title>Web Research Results</title></head><body>{reportContent}</body></html>"
|
||||
|
||||
return self.formatAgentDocumentOutput(outputLabel, reportContent, contentType)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating narrative document: {str(e)}")
|
||||
# Create error document
|
||||
if formatType == "markdown":
|
||||
content = f"# Web Research Error\n\nAn error occurred: {str(e)}"
|
||||
elif formatType == "html":
|
||||
content = f"<html><body><h1>Web Research Error</h1><p>An error occurred: {str(e)}</p></body></html>"
|
||||
else:
|
||||
content = f"WEB RESEARCH ERROR\n\nAn error occurred: {str(e)}"
|
||||
|
||||
return self.formatAgentDocumentOutput(outputLabel, content, contentType)
|
||||
|
||||
async def _createJsonDocument(self, prompt: str, results: List[Dict[str, Any]],
|
||||
researchPlan: Dict[str, Any], outputLabel: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a JSON document from research results.
|
||||
|
||||
Args:
|
||||
prompt: Original research prompt
|
||||
results: Research results
|
||||
researchPlan: Research plan
|
||||
outputLabel: Output filename
|
||||
|
||||
Returns:
|
||||
Document object
|
||||
"""
|
||||
try:
|
||||
# Create structured data
|
||||
sourcesData = []
|
||||
for result in results:
|
||||
sourcesData.append({
|
||||
"title": result.get("title", "Untitled"),
|
||||
"url": result.get("url", ""),
|
||||
"summary": result.get("summary", ""),
|
||||
"snippet": result.get("snippet", ""),
|
||||
"sourceType": result.get("sourceType", "")
|
||||
})
|
||||
|
||||
# Create metadata
|
||||
metadata = {
|
||||
"query": prompt,
|
||||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"researchQuestions": researchPlan.get("researchQuestions", []),
|
||||
"searchTerms": researchPlan.get("searchTerms", [])
|
||||
}
|
||||
|
||||
# Compile complete report object
|
||||
jsonContent = {
|
||||
"metadata": metadata,
|
||||
"summary": researchPlan.get("feedback", "Web research results"),
|
||||
"sources": sourcesData
|
||||
}
|
||||
|
||||
# Convert to JSON string
|
||||
content = json.dumps(jsonContent, indent=2)
|
||||
|
||||
return self.formatAgentDocumentOutput(outputLabel, content, "application/json")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating JSON document: {str(e)}")
|
||||
return self.formatAgentDocumentOutput(outputLabel, json.dumps({"error": str(e)}), "application/json")
|
||||
|
||||
async def _createCsvDocument(self, results: List[Dict[str, Any]], outputLabel: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a CSV document from research results.
|
||||
|
||||
Args:
|
||||
results: Research results
|
||||
outputLabel: Output filename
|
||||
|
||||
Returns:
|
||||
Document object
|
||||
"""
|
||||
try:
|
||||
# Create CSV header
|
||||
csvLines = ["Title,URL,Source Type,Snippet"]
|
||||
|
||||
# Add results
|
||||
for result in results:
|
||||
# Escape CSV fields
|
||||
title = result.get("title", "").replace('"', '""')
|
||||
url = result.get("url", "").replace('"', '""')
|
||||
sourceType = result.get("sourceType", "").replace('"', '""')
|
||||
snippet = result.get("snippet", "").replace('"', '""')
|
||||
|
||||
csvLines.append(f'"{title}","{url}","{sourceType}","{snippet}"')
|
||||
|
||||
# Combine into CSV content
|
||||
content = "\n".join(csvLines)
|
||||
|
||||
return self.formatAgentDocumentOutput(outputLabel, content, "text/csv")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating CSV document: {str(e)}")
|
||||
return self.formatAgentDocumentOutput(outputLabel, "Error,Error\nFailed to create CSV,{0}".format(str(e)), "text/csv")
|
||||
|
||||
def _determineFormatType(self, outputLabel: str) -> str:
|
||||
"""
|
||||
Determine the format type based on the filename.
|
||||
|
||||
Args:
|
||||
outputLabel: Output filename
|
||||
|
||||
Returns:
|
||||
Format type (markdown, html, text, json, csv)
|
||||
"""
|
||||
outputLabelLower = outputLabel.lower()
|
||||
|
||||
if outputLabelLower.endswith(".md"):
|
||||
return "markdown"
|
||||
elif outputLabelLower.endswith(".html"):
|
||||
return "html"
|
||||
elif outputLabelLower.endswith(".txt"):
|
||||
return "text"
|
||||
elif outputLabelLower.endswith(".json"):
|
||||
return "json"
|
||||
elif outputLabelLower.endswith(".csv"):
|
||||
return "csv"
|
||||
else:
|
||||
# Default to markdown
|
||||
return "markdown"
|
||||
|
||||
def _searchWeb(self, query: str) -> List[Dict[str, str]]:
|
||||
"""
|
||||
Conduct a web search using SerpAPI and return the results.
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
|
||||
Returns:
|
||||
List of search results
|
||||
"""
|
||||
if not self.srcApikey:
|
||||
return []
|
||||
|
||||
# Get user language from serviceBase if available
|
||||
userLanguage = "en" # Default language
|
||||
if self.service.base.userLanguage:
|
||||
userLanguage = self.service.base.userLanguage
|
||||
|
||||
try:
|
||||
# Format the search request for SerpAPI
|
||||
params = {
|
||||
"engine": self.srcEngine,
|
||||
"q": query,
|
||||
"api_key": self.srcApikey,
|
||||
"num": self.maxResults, # Number of results to return
|
||||
"hl": userLanguage # Identified user language
|
||||
}
|
||||
|
||||
# Make the API request
|
||||
response = requests.get("https://serpapi.com/search", params=params, timeout=self.timeout)
|
||||
response.raise_for_status()
|
||||
|
||||
# Parse JSON response
|
||||
search_results = response.json()
|
||||
|
||||
# Extract organic results
|
||||
results = []
|
||||
|
||||
if "organic_results" in search_results:
|
||||
for result in search_results["organic_results"][:self.maxResults]:
|
||||
# Extract title
|
||||
title = result.get("title", "No title")
|
||||
|
||||
# Extract URL
|
||||
url = result.get("link", "No URL")
|
||||
|
||||
# Extract snippet
|
||||
snippet = result.get("snippet", "No description")
|
||||
|
||||
# Get actual page content
|
||||
try:
|
||||
targetPageSoup = self._readUrl(url)
|
||||
content = self._extractMainContent(targetPageSoup)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error extracting content from {url}: {str(e)}")
|
||||
content = f"Error extracting content: {str(e)}"
|
||||
|
||||
results.append({
|
||||
'title': title,
|
||||
'url': url,
|
||||
'snippet': snippet,
|
||||
'data': content
|
||||
})
|
||||
|
||||
# Limit number of results
|
||||
if len(results) >= self.maxResults:
|
||||
break
|
||||
else:
|
||||
logger.warning(f"No organic results found in SerpAPI response for: {query}")
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching with SerpAPI for {query}: {str(e)}")
|
||||
return []
|
||||
|
||||
def _readUrl(self, url: str) -> BeautifulSoup:
|
||||
"""
|
||||
Read a URL and return a BeautifulSoup parser for the content.
|
||||
|
||||
Args:
|
||||
url: The URL to read
|
||||
|
||||
Returns:
|
||||
BeautifulSoup object with the content or None on errors
|
||||
"""
|
||||
if not url or not url.startswith(('http://', 'https://')):
|
||||
return None
|
||||
|
||||
headers = {
|
||||
'User-Agent': self.userAgent,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
}
|
||||
|
||||
try:
|
||||
# Initial request
|
||||
response = requests.get(url, headers=headers, timeout=self.timeout)
|
||||
|
||||
# Handling for status 202
|
||||
if response.status_code == 202:
|
||||
# Retry with backoff
|
||||
backoffTimes = [0.5, 1.0, 2.0, 5.0]
|
||||
|
||||
for waitTime in backoffTimes:
|
||||
time.sleep(waitTime)
|
||||
response = requests.get(url, headers=headers, timeout=self.timeout)
|
||||
|
||||
if response.status_code != 202:
|
||||
break
|
||||
|
||||
# Raise for error status codes
|
||||
response.raise_for_status()
|
||||
|
||||
# Parse HTML
|
||||
return BeautifulSoup(response.text, 'html.parser')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading URL {url}: {str(e)}")
|
||||
return None
|
||||
|
||||
def _extractTitle(self, soup: BeautifulSoup, url: str) -> str:
|
||||
"""
|
||||
Extract the title from a webpage.
|
||||
|
||||
Args:
|
||||
soup: BeautifulSoup object of the webpage
|
||||
url: URL of the webpage
|
||||
|
||||
Returns:
|
||||
Extracted title
|
||||
"""
|
||||
if not soup:
|
||||
return f"Error with {url}"
|
||||
|
||||
# Extract title from title tag
|
||||
titleTag = soup.find('title')
|
||||
title = titleTag.text.strip() if titleTag else "No title"
|
||||
|
||||
# Alternative: Also look for h1 tags if title tag is missing
|
||||
if title == "No title":
|
||||
h1Tag = soup.find('h1')
|
||||
if h1Tag:
|
||||
title = h1Tag.text.strip()
|
||||
|
||||
return title
|
||||
|
||||
def _extractMainContent(self, soup: BeautifulSoup, maxChars: int = 10000) -> str:
|
||||
"""
|
||||
Extract the main content from an HTML page.
|
||||
|
||||
Args:
|
||||
soup: BeautifulSoup object of the webpage
|
||||
maxChars: Maximum number of characters
|
||||
|
||||
Returns:
|
||||
Extracted main content as a string
|
||||
"""
|
||||
if not soup:
|
||||
return ""
|
||||
|
||||
# Try to find main content elements in priority order
|
||||
mainContent = None
|
||||
for selector in ['main', 'article', '#content', '.content', '#main', '.main']:
|
||||
content = soup.select_one(selector)
|
||||
if content:
|
||||
mainContent = content
|
||||
break
|
||||
|
||||
# If no main content found, use the body
|
||||
if not mainContent:
|
||||
mainContent = soup.find('body') or soup
|
||||
|
||||
# Remove script, style, nav, footer elements that don't contribute to main content
|
||||
for element in mainContent.select('script, style, nav, footer, header, aside, .sidebar, #sidebar, .comments, #comments, .advertisement, .ads, iframe'):
|
||||
element.extract()
|
||||
|
||||
# Extract text content
|
||||
textContent = mainContent.get_text(separator=' ', strip=True)
|
||||
|
||||
# Limit to maxChars
|
||||
return textContent[:maxChars]
|
||||
|
||||
def _limitText(self, text: str, maxChars: int = 10000) -> str:
|
||||
"""
|
||||
Limit text to a maximum number of characters.
|
||||
|
||||
Args:
|
||||
text: Input text
|
||||
maxChars: Maximum number of characters
|
||||
|
||||
Returns:
|
||||
Limited text
|
||||
"""
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
# If text is already under the limit, return unchanged
|
||||
if len(text) <= maxChars:
|
||||
return text
|
||||
|
||||
# Otherwise limit text to maxChars
|
||||
return text[:maxChars] + "... [Content truncated due to length]"
|
||||
|
||||
|
||||
# Factory function for the Webcrawler agent
|
||||
def getAgentWebcrawler():
|
||||
"""Returns an instance of the Webcrawler agent."""
|
||||
return AgentWebcrawler()
|
||||
|
|
@ -12,18 +12,18 @@ from modules.shared.attributeUtils import register_model_labels, ModelMixin
|
|||
|
||||
# ===== Method Models =====
|
||||
|
||||
class MethodResult(BaseModel, ModelMixin):
|
||||
"""Model for method results"""
|
||||
class ActionResult(BaseModel, ModelMixin):
|
||||
"""Model for action results from a methods action"""
|
||||
success: bool = Field(description="Whether the method execution was successful")
|
||||
data: Dict[str, Any] = Field(description="Result data")
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
||||
validation: List[str] = Field(default_factory=list, description="Validation messages")
|
||||
error: Optional[str] = Field(None, description="Error message if any")
|
||||
|
||||
# Register labels for MethodResult
|
||||
# Register labels for ActionResult
|
||||
register_model_labels(
|
||||
"MethodResult",
|
||||
{"en": "Method Result", "fr": "Résultat de méthode"},
|
||||
"ActionResult",
|
||||
{"en": "Action Result", "fr": "Résultat de l'action"},
|
||||
{
|
||||
"success": {"en": "Success", "fr": "Succès"},
|
||||
"data": {"en": "Data", "fr": "Données"},
|
||||
|
|
@ -174,6 +174,8 @@ class TaskAction(BaseModel, ModelMixin):
|
|||
retryMax: int = Field(default=3, description="Maximum number of retries")
|
||||
processingTime: Optional[float] = Field(None, description="Processing time in seconds")
|
||||
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC), description="When the action was executed")
|
||||
result: Optional[str] = Field(None, description="Result of the action")
|
||||
resultDocuments: Optional[List[ChatDocument]] = Field(None, description="Result documents from the action")
|
||||
|
||||
def isSuccessful(self) -> bool:
|
||||
"""Check if action was successful"""
|
||||
|
|
@ -206,14 +208,36 @@ register_model_labels(
|
|||
"execMethod": {"en": "Method", "fr": "Méthode"},
|
||||
"execAction": {"en": "Action", "fr": "Action"},
|
||||
"execParameters": {"en": "Parameters", "fr": "Paramètres"},
|
||||
"execResultLabel": {"en": "Result Label", "fr": "Label du résultat"},
|
||||
"status": {"en": "Status", "fr": "Statut"},
|
||||
"error": {"en": "Error", "fr": "Erreur"},
|
||||
"retryCount": {"en": "Retry Count", "fr": "Nombre de tentatives"},
|
||||
"retryMax": {"en": "Max Retries", "fr": "Tentatives max"},
|
||||
"resultDocuments": {"en": "Result Documents", "fr": "Documents du résultat"},
|
||||
"execResultLabel": {"en": "Document Label", "fr": "Label du document"},
|
||||
"processingTime": {"en": "Processing Time", "fr": "Temps de traitement"},
|
||||
"timestamp": {"en": "Timestamp", "fr": "Horodatage"}
|
||||
"timestamp": {"en": "Timestamp", "fr": "Horodatage"},
|
||||
"result": {"en": "Result", "fr": "Résultat"},
|
||||
"resultDocuments": {"en": "Result Documents", "fr": "Documents de résultat"}
|
||||
}
|
||||
)
|
||||
|
||||
class TaskResult(BaseModel, ModelMixin):
|
||||
"""Model for task results"""
|
||||
taskId: str = Field(..., description="Task ID")
|
||||
status: TaskStatus = Field(default=TaskStatus.PENDING, description="Task status")
|
||||
success: bool = Field(..., description="Whether the task was successful")
|
||||
feedback: Optional[str] = Field(None, description="Task feedback message")
|
||||
error: Optional[str] = Field(None, description="Error message if task failed")
|
||||
|
||||
# Register labels for TaskResult
|
||||
register_model_labels(
|
||||
"TaskResult",
|
||||
{"en": "Task Result", "fr": "Résultat de tâche"},
|
||||
{
|
||||
"taskId": {"en": "Task ID", "fr": "ID de la tâche"},
|
||||
"status": {"en": "Status", "fr": "Statut"},
|
||||
"success": {"en": "Success", "fr": "Succès"},
|
||||
"feedback": {"en": "Feedback", "fr": "Retour"},
|
||||
"error": {"en": "Error", "fr": "Erreur"}
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ Uses the JSON connector for data access with added language support.
|
|||
|
||||
import os
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime, UTC
|
||||
from typing import Dict, Any, List, Optional, Union
|
||||
|
||||
|
|
@ -15,11 +14,10 @@ from modules.interfaces.interfaceComponentAccess import ComponentAccess
|
|||
from modules.interfaces.interfaceComponentModel import (
|
||||
FilePreview, Prompt, FileItem, FileData
|
||||
)
|
||||
from modules.interfaces.interfaceAppModel import User, Mandate, UserPrivilege
|
||||
from modules.interfaces.interfaceAppModel import User
|
||||
|
||||
# DYNAMIC PART: Connectors to the Interface
|
||||
from modules.connectors.connectorDbJson import DatabaseConnector
|
||||
from modules.connectors.connectorAiOpenai import ChatService
|
||||
|
||||
# Basic Configurations
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
|
|
@ -61,7 +59,6 @@ class ComponentObjects:
|
|||
self.currentUser: Optional[User] = None
|
||||
self.userId: Optional[str] = None
|
||||
self.access: Optional[ComponentAccess] = None # Will be set when user context is provided
|
||||
self.aiService: Optional[ChatService] = None # Will be set when user context is provided
|
||||
|
||||
# Initialize database
|
||||
self._initializeDatabase()
|
||||
|
|
@ -87,9 +84,6 @@ class ComponentObjects:
|
|||
# Initialize access control with user context
|
||||
self.access = ComponentAccess(self.currentUser, self.db)
|
||||
|
||||
# Initialize AI service
|
||||
self.aiService = ChatService()
|
||||
|
||||
# Update database context
|
||||
self.db.updateContext(self.userId)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from typing import Dict, List, Optional, Any, Literal
|
|||
from datetime import datetime, UTC
|
||||
from pydantic import BaseModel, Field
|
||||
import logging
|
||||
from modules.interfaces.interfaceChatModel import MethodResult
|
||||
from modules.interfaces.interfaceChatModel import ActionResult
|
||||
from functools import wraps
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -11,8 +11,8 @@ logger = logging.getLogger(__name__)
|
|||
def action(func):
|
||||
"""Decorator to mark a method as an available action"""
|
||||
@wraps(func)
|
||||
async def wrapper(self, *args, **kwargs):
|
||||
return await func(self, *args, **kwargs)
|
||||
async def wrapper(self, parameters: Dict[str, Any], *args, **kwargs):
|
||||
return await func(self, parameters, *args, **kwargs)
|
||||
wrapper.is_action = True
|
||||
return wrapper
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ class MethodBase:
|
|||
"""Available actions and their parameters"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def execute(self, action: str, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
async def execute(self, action: str, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> ActionResult:
|
||||
"""
|
||||
Execute method action with authentication data
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ class MethodBase:
|
|||
authData: Authentication data
|
||||
|
||||
Returns:
|
||||
MethodResult containing execution results
|
||||
ActionResult containing execution results
|
||||
|
||||
Raises:
|
||||
ValueError: If action is not supported
|
||||
|
|
@ -79,7 +79,7 @@ class MethodBase:
|
|||
error=str(e)
|
||||
)
|
||||
|
||||
async def _executeAction(self, action: str, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
async def _executeAction(self, action: str, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> ActionResult:
|
||||
"""Execute specific action - to be implemented by subclasses"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
|
@ -109,9 +109,9 @@ class MethodBase:
|
|||
"""Rollback specific action - to be implemented by subclasses"""
|
||||
pass
|
||||
|
||||
def _createResult(self, success: bool, data: Dict[str, Any], metadata: Optional[Dict[str, Any]] = None, error: Optional[str] = None) -> MethodResult:
|
||||
def _createResult(self, success: bool, data: Dict[str, Any], metadata: Optional[Dict[str, Any]] = None, error: Optional[str] = None) -> ActionResult:
|
||||
"""Create a method result"""
|
||||
return MethodResult(
|
||||
return ActionResult(
|
||||
success=success,
|
||||
data=data,
|
||||
metadata=metadata or {},
|
||||
|
|
@ -119,6 +119,6 @@ class MethodBase:
|
|||
error=error
|
||||
)
|
||||
|
||||
def _addValidationMessage(self, result: MethodResult, message: str) -> None:
|
||||
def _addValidationMessage(self, result: ActionResult, message: str) -> None:
|
||||
"""Add a validation message to the result"""
|
||||
result.validation.append(message)
|
||||
|
|
@ -1,208 +1,246 @@
|
|||
from typing import Dict, Any, Optional
|
||||
import logging
|
||||
import ast
|
||||
import re
|
||||
from datetime import datetime, UTC
|
||||
|
||||
from modules.methods.methodBase import MethodBase, MethodResult, action
|
||||
from modules.methods.methodBase import MethodBase, ActionResult, action
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CoderService:
|
||||
"""Service for code analysis, generation, and refactoring operations"""
|
||||
|
||||
def __init__(self, serviceContainer: Any):
|
||||
self.serviceContainer = serviceContainer
|
||||
|
||||
async def analyzeCode(self, code: str, language: str = "python", checks: list = None) -> Dict[str, Any]:
|
||||
"""Analyze code quality and structure"""
|
||||
if checks is None:
|
||||
checks = ["complexity", "style", "security"]
|
||||
|
||||
try:
|
||||
# Create analysis prompt
|
||||
analysis_prompt = f"""
|
||||
Analyze this {language} code for quality, structure, and potential issues.
|
||||
|
||||
Code to analyze:
|
||||
{code}
|
||||
|
||||
Please check for:
|
||||
{', '.join(checks)}
|
||||
|
||||
Provide a detailed analysis including:
|
||||
1. Code quality assessment
|
||||
2. Potential issues and improvements
|
||||
3. Security considerations
|
||||
4. Performance optimizations
|
||||
5. Best practices compliance
|
||||
"""
|
||||
|
||||
# Use AI service for analysis
|
||||
analysis_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(analysis_prompt)
|
||||
|
||||
return {
|
||||
"language": language,
|
||||
"checks": checks,
|
||||
"analysis": analysis_result,
|
||||
"timestamp": datetime.now(UTC).isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing code: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"language": language,
|
||||
"checks": checks
|
||||
}
|
||||
|
||||
async def generateCode(self, requirements: str, language: str = "python", template: str = None) -> Dict[str, Any]:
|
||||
"""Generate code based on requirements"""
|
||||
try:
|
||||
# Create generation prompt
|
||||
generation_prompt = f"""
|
||||
Generate {language} code based on the following requirements:
|
||||
|
||||
Requirements:
|
||||
{requirements}
|
||||
|
||||
{f'Template to follow: {template}' if template else ''}
|
||||
|
||||
Please provide:
|
||||
1. Complete, working code
|
||||
2. Clear comments and documentation
|
||||
3. Error handling where appropriate
|
||||
4. Best practices implementation
|
||||
"""
|
||||
|
||||
# Use AI service for code generation
|
||||
generated_code = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(generation_prompt)
|
||||
|
||||
return {
|
||||
"language": language,
|
||||
"requirements": requirements,
|
||||
"code": generated_code,
|
||||
"timestamp": datetime.now(UTC).isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating code: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"language": language,
|
||||
"requirements": requirements
|
||||
}
|
||||
|
||||
async def refactorCode(self, code: str, language: str = "python", improvements: list = None) -> Dict[str, Any]:
|
||||
"""Refactor code for better quality"""
|
||||
if improvements is None:
|
||||
improvements = ["style", "complexity"]
|
||||
|
||||
try:
|
||||
# Create refactoring prompt
|
||||
refactor_prompt = f"""
|
||||
Refactor this {language} code to improve:
|
||||
{', '.join(improvements)}
|
||||
|
||||
Original code:
|
||||
{code}
|
||||
|
||||
Please provide:
|
||||
1. Refactored code with improvements
|
||||
2. Explanation of changes made
|
||||
3. Benefits of the refactoring
|
||||
4. Any potential trade-offs
|
||||
"""
|
||||
|
||||
# Use AI service for refactoring
|
||||
refactored_code = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(refactor_prompt)
|
||||
|
||||
return {
|
||||
"language": language,
|
||||
"improvements": improvements,
|
||||
"original_code": code,
|
||||
"refactored_code": refactored_code,
|
||||
"timestamp": datetime.now(UTC).isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error refactoring code: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"language": language,
|
||||
"improvements": improvements
|
||||
}
|
||||
|
||||
class MethodCoder(MethodBase):
|
||||
"""Coder method implementation for code operations"""
|
||||
|
||||
def __init__(self, serviceContainer: Any):
|
||||
super().__init__(serviceContainer)
|
||||
self.name = "coder"
|
||||
self.description = "Handle code operations like analysis, generation, and refactoring"
|
||||
self.description = "Handle code operations like analysis and generation"
|
||||
self.coderService = CoderService(serviceContainer)
|
||||
|
||||
@action
|
||||
async def analyze(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""Analyze code structure and quality"""
|
||||
async def analyze(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Analyze code quality and structure"""
|
||||
try:
|
||||
code = parameters["code"]
|
||||
code = parameters.get("code")
|
||||
language = parameters.get("language", "python")
|
||||
metrics = parameters.get("metrics", ["complexity", "style", "documentation"])
|
||||
checks = parameters.get("checks", ["complexity", "style", "security"])
|
||||
|
||||
analysis = {}
|
||||
|
||||
if language.lower() == "python":
|
||||
# Parse Python code
|
||||
try:
|
||||
tree = ast.parse(code)
|
||||
|
||||
# Calculate basic metrics
|
||||
analysis["metrics"] = {
|
||||
"lines": len(code.splitlines()),
|
||||
"classes": len([node for node in ast.walk(tree) if isinstance(node, ast.ClassDef)]),
|
||||
"functions": len([node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)]),
|
||||
"imports": len([node for node in ast.walk(tree) if isinstance(node, ast.Import) or isinstance(node, ast.ImportFrom)])
|
||||
}
|
||||
|
||||
# Check for common issues
|
||||
analysis["issues"] = []
|
||||
|
||||
# Check for missing docstrings
|
||||
if "documentation" in metrics:
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, (ast.ClassDef, ast.FunctionDef)) and not ast.get_docstring(node):
|
||||
analysis["issues"].append({
|
||||
"type": "missing_docstring",
|
||||
"line": node.lineno,
|
||||
"name": node.name
|
||||
})
|
||||
|
||||
# Check for long functions
|
||||
if "complexity" in metrics:
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
bodyLines = len(node.body)
|
||||
if bodyLines > 20: # Arbitrary threshold
|
||||
analysis["issues"].append({
|
||||
"type": "long_function",
|
||||
"line": node.lineno,
|
||||
"name": node.name,
|
||||
"lines": bodyLines
|
||||
})
|
||||
|
||||
# Check for style issues
|
||||
if "style" in metrics:
|
||||
# Check line length
|
||||
for i, line in enumerate(code.splitlines(), 1):
|
||||
if len(line) > 100: # PEP 8 recommendation
|
||||
analysis["issues"].append({
|
||||
"type": "line_too_long",
|
||||
"line": i,
|
||||
"length": len(line)
|
||||
})
|
||||
|
||||
# Check for mixed tabs and spaces
|
||||
if "\t" in code and " " in code:
|
||||
analysis["issues"].append({
|
||||
"type": "mixed_tabs_spaces",
|
||||
"message": "Code mixes tabs and spaces"
|
||||
})
|
||||
|
||||
except SyntaxError as e:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"Syntax error: {str(e)}"}
|
||||
)
|
||||
else:
|
||||
# TODO: Implement analysis for other languages
|
||||
if not code:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"Unsupported language: {language}"}
|
||||
data={},
|
||||
error="Code is required"
|
||||
)
|
||||
|
||||
# Analyze code
|
||||
results = await self.coderService.analyzeCode(
|
||||
code=code,
|
||||
language=language,
|
||||
checks=checks
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"language": language,
|
||||
"analysis": analysis
|
||||
}
|
||||
data=results
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing code: {e}")
|
||||
logger.error(f"Error analyzing code: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"Analysis failed: {str(e)}"}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def generate(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
async def generate(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Generate code based on requirements"""
|
||||
try:
|
||||
requirements = parameters["requirements"]
|
||||
requirements = parameters.get("requirements")
|
||||
language = parameters.get("language", "python")
|
||||
style = parameters.get("style", "standard")
|
||||
template = parameters.get("template")
|
||||
|
||||
# TODO: Implement code generation using AI or templates
|
||||
# This is a placeholder implementation
|
||||
if language.lower() == "python":
|
||||
# Generate a simple Python class based on requirements
|
||||
className = re.sub(r'[^a-zA-Z0-9]', '', requirements.split()[0].title())
|
||||
code = f"""class {className}:
|
||||
\"\"\"
|
||||
{requirements}
|
||||
\"\"\"
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def process(self):
|
||||
pass
|
||||
"""
|
||||
else:
|
||||
if not requirements:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"Unsupported language: {language}"}
|
||||
data={},
|
||||
error="Requirements are required"
|
||||
)
|
||||
|
||||
# Generate code
|
||||
code = await self.coderService.generateCode(
|
||||
requirements=requirements,
|
||||
language=language,
|
||||
template=template
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"language": language,
|
||||
"code": code
|
||||
}
|
||||
data=code
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating code: {e}")
|
||||
logger.error(f"Error generating code: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"Generation failed: {str(e)}"}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def refactor(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
async def refactor(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Refactor code for better quality"""
|
||||
try:
|
||||
code = parameters["code"]
|
||||
code = parameters.get("code")
|
||||
language = parameters.get("language", "python")
|
||||
improvements = parameters.get("improvements", ["style", "complexity"])
|
||||
|
||||
if language.lower() == "python":
|
||||
# Parse Python code
|
||||
try:
|
||||
tree = ast.parse(code)
|
||||
|
||||
# Apply improvements
|
||||
if "style" in improvements:
|
||||
# Format code (placeholder)
|
||||
code = code.strip()
|
||||
|
||||
if "complexity" in improvements:
|
||||
# TODO: Implement complexity reduction
|
||||
pass
|
||||
|
||||
if "documentation" in improvements:
|
||||
# Add missing docstrings
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, (ast.ClassDef, ast.FunctionDef)) and not ast.get_docstring(node):
|
||||
# TODO: Generate docstring
|
||||
pass
|
||||
|
||||
except SyntaxError as e:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"Syntax error: {str(e)}"}
|
||||
)
|
||||
else:
|
||||
if not code:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"Unsupported language: {language}"}
|
||||
data={},
|
||||
error="Code is required"
|
||||
)
|
||||
|
||||
# Refactor code
|
||||
result = await self.coderService.refactorCode(
|
||||
code=code,
|
||||
language=language,
|
||||
improvements=improvements
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"language": language,
|
||||
"code": code,
|
||||
"improvements": improvements
|
||||
}
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error refactoring code: {e}")
|
||||
logger.error(f"Error refactoring code: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"Refactoring failed: {str(e)}"}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
|
@ -5,198 +5,281 @@ Handles document operations using the document service.
|
|||
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from modules.interfaces.interfaceChatModel import (
|
||||
ChatDocument,
|
||||
TaskDocument,
|
||||
ExtractedContent,
|
||||
ContentItem
|
||||
)
|
||||
from modules.workflow.managerDocument import DocumentManager
|
||||
from modules.methods.methodBase import MethodBase, MethodResult, action
|
||||
from modules.methods.methodBase import MethodBase, ActionResult, action
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MethodDocument(MethodBase):
|
||||
"""Document processing method implementation"""
|
||||
class DocumentService:
|
||||
"""Service for document content extraction, analysis, and summarization"""
|
||||
|
||||
def __init__(self, serviceContainer):
|
||||
def __init__(self, serviceContainer: Any):
|
||||
self.serviceContainer = serviceContainer
|
||||
|
||||
async def extractContent(self, fileId: str, format: str = "text", includeMetadata: bool = True) -> Dict[str, Any]:
|
||||
"""Extract content from document using prompt-based extraction"""
|
||||
try:
|
||||
# Get file data
|
||||
file_data = self.serviceContainer.getFileData(fileId)
|
||||
file_info = self.serviceContainer.getFileInfo(fileId)
|
||||
|
||||
if not file_data:
|
||||
return {
|
||||
"error": "File not found or empty",
|
||||
"fileId": fileId
|
||||
}
|
||||
|
||||
# Create extraction prompt based on format
|
||||
extraction_prompt = f"""
|
||||
Extract and structure the content from this document.
|
||||
|
||||
File information:
|
||||
- Name: {file_info.get('name', 'Unknown')}
|
||||
- Type: {file_info.get('mimeType', 'Unknown')}
|
||||
- Size: {len(file_data)} bytes
|
||||
|
||||
Please extract:
|
||||
1. Main content and key information
|
||||
2. Structured data if present (tables, lists, etc.)
|
||||
3. Important facts and figures
|
||||
4. Key insights and takeaways
|
||||
|
||||
Format the output as: {format}
|
||||
Include metadata: {includeMetadata}
|
||||
"""
|
||||
|
||||
# Use the new direct file data extraction method
|
||||
extracted_content = await self.serviceContainer.extractContentFromFileData(
|
||||
prompt=extraction_prompt,
|
||||
fileData=file_data,
|
||||
filename=file_info.get('name', 'document'),
|
||||
mimeType=file_info.get('mimeType', 'application/octet-stream'),
|
||||
base64Encoded=False
|
||||
)
|
||||
|
||||
result = {
|
||||
"fileId": fileId,
|
||||
"format": format,
|
||||
"content": extracted_content,
|
||||
"fileInfo": file_info if includeMetadata else None
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting content: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"fileId": fileId
|
||||
}
|
||||
|
||||
async def analyzeContent(self, fileId: str, analysis: list = None) -> Dict[str, Any]:
|
||||
"""Analyze document content for entities, topics, and sentiment"""
|
||||
if analysis is None:
|
||||
analysis = ["entities", "topics", "sentiment"]
|
||||
|
||||
try:
|
||||
# First extract content
|
||||
content_result = await self.extractContent(fileId, "text", True)
|
||||
|
||||
if "error" in content_result:
|
||||
return content_result
|
||||
|
||||
content = content_result.get("content", "")
|
||||
|
||||
# Create analysis prompt
|
||||
analysis_prompt = f"""
|
||||
Analyze this document content for the following aspects:
|
||||
{', '.join(analysis)}
|
||||
|
||||
Document content:
|
||||
{content[:5000]} # Limit content length
|
||||
|
||||
Please provide a detailed analysis including:
|
||||
1. Key entities (people, organizations, locations, dates)
|
||||
2. Main topics and themes
|
||||
3. Sentiment analysis (positive, negative, neutral)
|
||||
4. Key insights and patterns
|
||||
5. Important relationships between entities
|
||||
6. Document structure and organization
|
||||
"""
|
||||
|
||||
# Use AI service for analysis
|
||||
analysis_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(analysis_prompt)
|
||||
|
||||
return {
|
||||
"fileId": fileId,
|
||||
"analysis": analysis,
|
||||
"results": analysis_result,
|
||||
"content": content_result
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing content: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"fileId": fileId,
|
||||
"analysis": analysis
|
||||
}
|
||||
|
||||
async def summarizeContent(self, fileId: str, maxLength: int = 200, format: str = "text") -> Dict[str, Any]:
|
||||
"""Summarize document content"""
|
||||
try:
|
||||
# First extract content
|
||||
content_result = await self.extractContent(fileId, "text", False)
|
||||
|
||||
if "error" in content_result:
|
||||
return content_result
|
||||
|
||||
content = content_result.get("content", "")
|
||||
|
||||
# Create summarization prompt
|
||||
summary_prompt = f"""
|
||||
Create a comprehensive summary of this document content.
|
||||
|
||||
Document content:
|
||||
{content[:8000]} # Limit content length
|
||||
|
||||
Requirements:
|
||||
- Maximum length: {maxLength} words
|
||||
- Format: {format}
|
||||
- Include key points and main ideas
|
||||
- Maintain accuracy and completeness
|
||||
- Use clear, professional language
|
||||
- Highlight important insights and conclusions
|
||||
"""
|
||||
|
||||
# Use AI service for summarization
|
||||
summary = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(summary_prompt)
|
||||
|
||||
return {
|
||||
"fileId": fileId,
|
||||
"maxLength": maxLength,
|
||||
"format": format,
|
||||
"summary": summary,
|
||||
"wordCount": len(summary.split()),
|
||||
"originalContent": content_result
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error summarizing content: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"fileId": fileId,
|
||||
"maxLength": maxLength
|
||||
}
|
||||
|
||||
class MethodDocument(MethodBase):
|
||||
"""Document method implementation for document operations"""
|
||||
|
||||
def __init__(self, serviceContainer: Any):
|
||||
"""Initialize the document method"""
|
||||
super().__init__(serviceContainer)
|
||||
self.name = "document"
|
||||
self.description = "Handle document operations like extraction and analysis"
|
||||
self.documentService = DocumentService(serviceContainer)
|
||||
self.documentManager = DocumentManager(serviceContainer)
|
||||
|
||||
@action
|
||||
async def extract(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Extract content from document
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
documentId: ID of the document to extract from
|
||||
documentType: Type of document
|
||||
extractionType: Type of extraction to perform
|
||||
"""
|
||||
async def extract(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Extract content from document"""
|
||||
try:
|
||||
documentId = parameters["documentId"]
|
||||
documentType = parameters.get("documentType", "text")
|
||||
extractionType = parameters.get("extractionType", "full")
|
||||
fileId = parameters.get("fileId")
|
||||
format = parameters.get("format", "text")
|
||||
includeMetadata = parameters.get("includeMetadata", True)
|
||||
|
||||
# Get document from service
|
||||
document = await self.service.interfaceComponent.getDocument(documentId)
|
||||
if not document:
|
||||
if not fileId:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"Document not found: {documentId}"}
|
||||
data={},
|
||||
error="File ID is required"
|
||||
)
|
||||
|
||||
# Extract content based on type
|
||||
if documentType == "text":
|
||||
content = await self.documentManager.extractTextContent(document, extractionType)
|
||||
elif documentType == "table":
|
||||
content = await self.documentManager.extractTableContent(document, extractionType)
|
||||
elif documentType == "image":
|
||||
content = await self.documentManager.extractImageContent(document, extractionType)
|
||||
else:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"Unsupported document type: {documentType}"}
|
||||
)
|
||||
# Extract content
|
||||
content = await self.documentService.extractContent(
|
||||
fileId=fileId,
|
||||
format=format,
|
||||
includeMetadata=includeMetadata
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"documentId": documentId,
|
||||
"type": documentType,
|
||||
"content": content
|
||||
}
|
||||
data=content
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting content: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def analyze(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Analyze document content
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
documentId: ID of the document to analyze
|
||||
documentType: Type of document
|
||||
analysisType: Type of analysis to perform
|
||||
"""
|
||||
async def analyze(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Analyze document content"""
|
||||
try:
|
||||
# Extract content first
|
||||
contentResult = await self.extract(parameters)
|
||||
if not contentResult.success:
|
||||
return contentResult
|
||||
fileId = parameters.get("fileId")
|
||||
analysis = parameters.get("analysis", ["entities", "topics", "sentiment"])
|
||||
|
||||
# Perform analysis based on type
|
||||
analysisType = parameters.get("analysisType", "basic")
|
||||
content = ExtractedContent(**contentResult.data["content"])
|
||||
|
||||
if analysisType == "basic":
|
||||
# Basic analysis: count items, calculate statistics
|
||||
stats = {
|
||||
"totalItems": len(content.contents),
|
||||
"totalSize": sum(item.metadata.size for item in content.contents),
|
||||
"itemTypes": {}
|
||||
}
|
||||
|
||||
for item in content.contents:
|
||||
itemType = item.label
|
||||
if itemType not in stats["itemTypes"]:
|
||||
stats["itemTypes"][itemType] = 0
|
||||
stats["itemTypes"][itemType] += 1
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"documentId": parameters["documentId"],
|
||||
"analysis": stats
|
||||
}
|
||||
)
|
||||
else:
|
||||
if not fileId:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"Unsupported analysis type: {analysisType}"}
|
||||
data={},
|
||||
error="File ID is required"
|
||||
)
|
||||
|
||||
|
||||
# Analyze content
|
||||
results = await self.documentService.analyzeContent(
|
||||
fileId=fileId,
|
||||
analysis=analysis
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data=results
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing document: {str(e)}")
|
||||
logger.error(f"Error analyzing content: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def summarize(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Summarize document content
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
documentId: ID of the document to summarize
|
||||
documentType: Type of document
|
||||
summaryType: Type of summary to generate
|
||||
"""
|
||||
async def summarize(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Summarize document content"""
|
||||
try:
|
||||
# Extract content first
|
||||
contentResult = await self.extract(parameters)
|
||||
if not contentResult.success:
|
||||
return contentResult
|
||||
fileId = parameters.get("fileId")
|
||||
maxLength = parameters.get("maxLength", 200)
|
||||
format = parameters.get("format", "text")
|
||||
|
||||
# Generate summary based on type
|
||||
summaryType = parameters.get("summaryType", "basic")
|
||||
content = ExtractedContent(**contentResult.data["content"])
|
||||
|
||||
if summaryType == "basic":
|
||||
# Basic summary: concatenate all text content
|
||||
summary = "\n".join(item.content for item in content.contents if item.content)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"documentId": parameters["documentId"],
|
||||
"summary": summary
|
||||
}
|
||||
)
|
||||
else:
|
||||
if not fileId:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"Unsupported summary type: {summaryType}"}
|
||||
data={},
|
||||
error="File ID is required"
|
||||
)
|
||||
|
||||
|
||||
# Summarize content
|
||||
summary = await self.documentService.summarizeContent(
|
||||
fileId=fileId,
|
||||
maxLength=maxLength,
|
||||
format=format
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data=summary
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error summarizing document: {str(e)}")
|
||||
logger.error(f"Error summarizing content: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
async def _getChatDocument(self, documentId: str) -> Optional[ChatDocument]:
|
||||
"""Get ChatDocument from database"""
|
||||
try:
|
||||
documentData = self.service.db.getRecord("chatDocuments", documentId)
|
||||
if documentData:
|
||||
return ChatDocument(**documentData)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting ChatDocument {documentId}: {str(e)}")
|
||||
return None
|
||||
|
||||
async def _getTaskDocument(self, documentId: str) -> Optional[TaskDocument]:
|
||||
"""Get TaskDocument from database"""
|
||||
try:
|
||||
documentData = self.service.db.getRecord("taskDocuments", documentId)
|
||||
if documentData:
|
||||
return TaskDocument(**documentData)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting TaskDocument {documentId}: {str(e)}")
|
||||
return None
|
||||
|
|
@ -5,184 +5,415 @@ Handles Excel operations using the Excel service.
|
|||
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from datetime import datetime, UTC
|
||||
import json
|
||||
import base64
|
||||
|
||||
from modules.interfaces.interfaceExcel import ExcelService
|
||||
from modules.methods.methodBase import MethodBase, MethodResult, action
|
||||
from modules.methods.methodBase import MethodBase, ActionResult, action
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MethodExcel(MethodBase):
|
||||
"""Excel method implementation"""
|
||||
class ExcelService:
|
||||
"""Service for Microsoft Excel operations using Graph API"""
|
||||
|
||||
def __init__(self, serviceContainer):
|
||||
def __init__(self, serviceContainer: Any):
|
||||
self.serviceContainer = serviceContainer
|
||||
|
||||
def _getMicrosoftConnection(self, connectionReference: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get Microsoft connection from connection reference"""
|
||||
try:
|
||||
userConnection = self.serviceContainer.getUserConnectionFromConnectionReference(connectionReference)
|
||||
if userConnection and userConnection.authority == "microsoft" and userConnection.enabled:
|
||||
return {
|
||||
"id": userConnection.id,
|
||||
"accessToken": userConnection.accessToken,
|
||||
"refreshToken": userConnection.refreshToken,
|
||||
"scopes": userConnection.scopes
|
||||
}
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Microsoft connection: {str(e)}")
|
||||
return None
|
||||
|
||||
async def readFile(self, fileId: str, connectionReference: str, sheetName: str = "Sheet1", range: str = None) -> Dict[str, Any]:
|
||||
"""Read data from Excel file using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"fileId": fileId,
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# Get file data from service container
|
||||
file_data = self.serviceContainer.getFileData(fileId)
|
||||
file_info = self.serviceContainer.getFileInfo(fileId)
|
||||
|
||||
if not file_data:
|
||||
return {
|
||||
"error": "File not found or empty",
|
||||
"fileId": fileId
|
||||
}
|
||||
|
||||
# For now, simulate Excel reading with AI analysis
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
excel_prompt = f"""
|
||||
Analyze this Excel file data and extract structured information.
|
||||
|
||||
File: {file_info.get('name', 'Unknown')}
|
||||
Sheet: {sheetName}
|
||||
Range: {range or 'All data'}
|
||||
|
||||
File content (first 5000 characters):
|
||||
{file_data.decode('utf-8', errors='ignore')[:5000] if isinstance(file_data, bytes) else str(file_data)[:5000]}
|
||||
|
||||
Please extract:
|
||||
1. All data from the specified sheet and range
|
||||
2. Column headers and data types
|
||||
3. Key metrics and calculations
|
||||
4. Any charts or visualizations described
|
||||
5. Summary statistics
|
||||
|
||||
Return the data in a structured JSON format.
|
||||
"""
|
||||
|
||||
# Use AI to analyze Excel content
|
||||
analysis_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(excel_prompt)
|
||||
|
||||
return {
|
||||
"fileId": fileId,
|
||||
"sheetName": sheetName,
|
||||
"range": range,
|
||||
"data": analysis_result,
|
||||
"fileInfo": file_info,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading Excel file: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"fileId": fileId
|
||||
}
|
||||
|
||||
async def writeFile(self, fileId: str, connectionReference: str, sheetName: str, data: Any, range: str = None) -> Dict[str, Any]:
|
||||
"""Write data to Excel file using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"fileId": fileId,
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate Excel writing
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
write_prompt = f"""
|
||||
Prepare data for writing to Excel file.
|
||||
|
||||
File: {fileId}
|
||||
Sheet: {sheetName}
|
||||
Range: {range or 'Auto-detect'}
|
||||
|
||||
Data to write:
|
||||
{json.dumps(data, indent=2)}
|
||||
|
||||
Please format this data appropriately for Excel and provide:
|
||||
1. Structured data ready for Excel
|
||||
2. Column headers and formatting
|
||||
3. Any formulas or calculations needed
|
||||
4. Data validation rules if applicable
|
||||
"""
|
||||
|
||||
# Use AI to prepare Excel data
|
||||
prepared_data = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(write_prompt)
|
||||
|
||||
return {
|
||||
"fileId": fileId,
|
||||
"sheetName": sheetName,
|
||||
"range": range,
|
||||
"data": prepared_data,
|
||||
"status": "prepared",
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error writing to Excel file: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"fileId": fileId
|
||||
}
|
||||
|
||||
async def createFile(self, fileName: str, connectionReference: str, template: str = None) -> Dict[str, Any]:
|
||||
"""Create new Excel file using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate file creation
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
create_prompt = f"""
|
||||
Create a new Excel file structure.
|
||||
|
||||
File name: {fileName}
|
||||
Template: {template or 'Standard'}
|
||||
|
||||
Please provide:
|
||||
1. Initial sheet structure
|
||||
2. Default column headers
|
||||
3. Sample data if template specified
|
||||
4. Formatting guidelines
|
||||
"""
|
||||
|
||||
# Use AI to create Excel structure
|
||||
file_structure = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(create_prompt)
|
||||
|
||||
# Create file using service container
|
||||
file_id = self.serviceContainer.createFile(
|
||||
fileName=fileName,
|
||||
mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
content=file_structure,
|
||||
base64encoded=False
|
||||
)
|
||||
|
||||
return {
|
||||
"fileId": file_id,
|
||||
"fileName": fileName,
|
||||
"template": template,
|
||||
"structure": file_structure,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating Excel file: {str(e)}")
|
||||
return {
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
async def formatCells(self, fileId: str, connectionReference: str, sheetName: str, range: str, format: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Format Excel cells using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"fileId": fileId,
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate formatting
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
format_prompt = f"""
|
||||
Apply formatting to Excel cells.
|
||||
|
||||
File: {fileId}
|
||||
Sheet: {sheetName}
|
||||
Range: {range}
|
||||
Format: {json.dumps(format, indent=2)}
|
||||
|
||||
Please provide:
|
||||
1. Applied formatting details
|
||||
2. Visual representation of the formatting
|
||||
3. Any conditional formatting rules
|
||||
"""
|
||||
|
||||
# Use AI to describe formatting
|
||||
formatting_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(format_prompt)
|
||||
|
||||
return {
|
||||
"fileId": fileId,
|
||||
"sheetName": sheetName,
|
||||
"range": range,
|
||||
"format": format,
|
||||
"result": formatting_result,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error formatting Excel cells: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"fileId": fileId
|
||||
}
|
||||
|
||||
class MethodExcel(MethodBase):
|
||||
"""Excel method implementation for spreadsheet operations"""
|
||||
|
||||
def __init__(self, serviceContainer: Any):
|
||||
"""Initialize the Excel method"""
|
||||
super().__init__(serviceContainer)
|
||||
self.name = "excel"
|
||||
self.description = "Handle Excel spreadsheet operations like reading and writing data"
|
||||
self.excelService = ExcelService(serviceContainer)
|
||||
|
||||
@action
|
||||
async def read(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Read data from Excel file
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
fileId: ID of the Excel file
|
||||
sheetName: Name of the sheet to read
|
||||
range: Cell range to read (e.g. "A1:B10")
|
||||
"""
|
||||
async def read(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Read data from Excel file"""
|
||||
try:
|
||||
fileId = parameters["fileId"]
|
||||
fileId = parameters.get("fileId")
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
sheetName = parameters.get("sheetName", "Sheet1")
|
||||
range = parameters.get("range")
|
||||
|
||||
# Get file from service
|
||||
file = await self.service.interfaceComponent.getFile(fileId)
|
||||
if not file:
|
||||
if not fileId or not connectionReference:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"File not found: {fileId}"}
|
||||
data={},
|
||||
error="File ID and connection reference are required"
|
||||
)
|
||||
|
||||
# Read data from Excel
|
||||
data = await self.excelService.readData(file, sheetName, range)
|
||||
data = await self.excelService.readFile(
|
||||
fileId=fileId,
|
||||
connectionReference=connectionReference,
|
||||
sheetName=sheetName,
|
||||
range=range
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"fileId": fileId,
|
||||
"sheetName": sheetName,
|
||||
"range": range,
|
||||
"data": data
|
||||
}
|
||||
data=data
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading Excel file: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def write(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Write data to Excel file
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
fileId: ID of the Excel file
|
||||
sheetName: Name of the sheet to write to
|
||||
range: Cell range to write to (e.g. "A1:B10")
|
||||
data: Data to write
|
||||
"""
|
||||
async def write(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Write data to Excel file"""
|
||||
try:
|
||||
fileId = parameters["fileId"]
|
||||
fileId = parameters.get("fileId")
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
sheetName = parameters.get("sheetName", "Sheet1")
|
||||
data = parameters.get("data")
|
||||
range = parameters.get("range")
|
||||
data = parameters["data"]
|
||||
|
||||
# Get file from service
|
||||
file = await self.service.interfaceComponent.getFile(fileId)
|
||||
if not file:
|
||||
if not fileId or not connectionReference or not data:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"File not found: {fileId}"}
|
||||
data={},
|
||||
error="File ID, connection reference, and data are required"
|
||||
)
|
||||
|
||||
# Write data to Excel
|
||||
await self.excelService.writeData(file, sheetName, range, data)
|
||||
result = await self.excelService.writeFile(
|
||||
fileId=fileId,
|
||||
connectionReference=connectionReference,
|
||||
sheetName=sheetName,
|
||||
data=data,
|
||||
range=range
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"fileId": fileId,
|
||||
"sheetName": sheetName,
|
||||
"range": range
|
||||
}
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error writing to Excel file: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def create(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Create new Excel file
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
fileName: Name of the new file
|
||||
sheets: List of sheet configurations
|
||||
"""
|
||||
async def create(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Create new Excel file"""
|
||||
try:
|
||||
fileName = parameters["fileName"]
|
||||
sheets = parameters.get("sheets", [{"name": "Sheet1"}])
|
||||
fileName = parameters.get("fileName")
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
template = parameters.get("template")
|
||||
|
||||
# Create new Excel file
|
||||
file = await self.excelService.createFile(fileName, sheets)
|
||||
if not fileName or not connectionReference:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="File name and connection reference are required"
|
||||
)
|
||||
|
||||
# Create Excel file
|
||||
fileId = await self.excelService.createFile(
|
||||
fileName=fileName,
|
||||
connectionReference=connectionReference,
|
||||
template=template
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"fileId": file.id,
|
||||
"fileName": fileName,
|
||||
"sheets": sheets
|
||||
}
|
||||
data={"fileId": fileId}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating Excel file: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def format(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Format Excel cells
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
fileId: ID of the Excel file
|
||||
sheetName: Name of the sheet to format
|
||||
range: Cell range to format (e.g. "A1:B10")
|
||||
format: Format configuration
|
||||
"""
|
||||
async def format(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Format Excel cells"""
|
||||
try:
|
||||
fileId = parameters["fileId"]
|
||||
fileId = parameters.get("fileId")
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
sheetName = parameters.get("sheetName", "Sheet1")
|
||||
range = parameters.get("range")
|
||||
format = parameters["format"]
|
||||
format = parameters.get("format")
|
||||
|
||||
# Get file from service
|
||||
file = await self.service.interfaceComponent.getFile(fileId)
|
||||
if not file:
|
||||
if not fileId or not connectionReference or not range or not format:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"File not found: {fileId}"}
|
||||
data={},
|
||||
error="File ID, connection reference, range, and format are required"
|
||||
)
|
||||
|
||||
# Apply formatting
|
||||
await self.excelService.formatCells(file, sheetName, range, format)
|
||||
result = await self.excelService.formatCells(
|
||||
fileId=fileId,
|
||||
connectionReference=connectionReference,
|
||||
sheetName=sheetName,
|
||||
range=range,
|
||||
format=format
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"fileId": fileId,
|
||||
"sheetName": sheetName,
|
||||
"range": range
|
||||
}
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error formatting Excel cells: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
|
@ -1,78 +1,156 @@
|
|||
"""Operator method implementation for handling collections and AI operations"""
|
||||
|
||||
from typing import Dict, List, Any, Optional
|
||||
from datetime import datetime, UTC
|
||||
import logging
|
||||
from .methodBase import MethodBase
|
||||
from modules.interfaces.interfaceChatModel import MethodResult
|
||||
|
||||
from modules.methods.methodBase import MethodBase, ActionResult, action
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class OperatorService:
|
||||
"""Service for operator operations like forEach and AI calls"""
|
||||
|
||||
def __init__(self, serviceContainer: Any):
|
||||
self.serviceContainer = serviceContainer
|
||||
|
||||
async def executeForEach(self, items: List[Any], action: Dict[str, Any]) -> List[Any]:
|
||||
"""Execute an action for each item in a list"""
|
||||
try:
|
||||
results = []
|
||||
|
||||
for i, item in enumerate(items):
|
||||
logger.info(f"Executing forEach action {i+1}/{len(items)}")
|
||||
|
||||
# Create context with current item
|
||||
context = {
|
||||
"item": item,
|
||||
"index": i,
|
||||
"total": len(items),
|
||||
"isFirst": i == 0,
|
||||
"isLast": i == len(items) - 1
|
||||
}
|
||||
|
||||
# Execute the action using the service container
|
||||
if "method" in action and "action" in action:
|
||||
methodName = action["method"]
|
||||
actionName = action["action"]
|
||||
parameters = action.get("parameters", {})
|
||||
|
||||
# Add context to parameters
|
||||
parameters["context"] = context
|
||||
parameters["currentItem"] = item
|
||||
|
||||
# Execute the method action
|
||||
result = await self.serviceContainer.executeAction(
|
||||
methodName=methodName,
|
||||
actionName=actionName,
|
||||
parameters=parameters
|
||||
)
|
||||
|
||||
# Return the exact result data, not wrapped
|
||||
if result.success:
|
||||
results.append(result.data)
|
||||
else:
|
||||
results.append({"error": result.error})
|
||||
else:
|
||||
# Simple action without method call
|
||||
results.append({"error": "No method specified"})
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing forEach: {str(e)}")
|
||||
return [{"error": str(e)}] * len(items) if items else []
|
||||
|
||||
async def executeAiCall(self, prompt: str, documents: List[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""Call AI service with document content"""
|
||||
try:
|
||||
# Prepare context from documents
|
||||
context = ""
|
||||
extractedDocuments = []
|
||||
|
||||
if documents:
|
||||
for i, doc in enumerate(documents):
|
||||
documentReference = doc.get('documentReference')
|
||||
contentExtractionPrompt = doc.get('contentExtractionPrompt', 'Extract the main content from this document')
|
||||
|
||||
if documentReference:
|
||||
# Get documents from reference
|
||||
chatDocuments = self.serviceContainer.getChatDocumentsFromDocumentReference(documentReference)
|
||||
|
||||
if chatDocuments:
|
||||
# Extract content from each document
|
||||
for j, chatDoc in enumerate(chatDocuments):
|
||||
try:
|
||||
# Extract content using the document manager
|
||||
extractedContent = await self.serviceContainer.documentManager.extractContentFromChatDocument(
|
||||
chatDocument=chatDoc,
|
||||
extractionPrompt=contentExtractionPrompt
|
||||
)
|
||||
|
||||
extractedDocuments.append({
|
||||
"documentReference": documentReference,
|
||||
"documentId": chatDoc.id,
|
||||
"extractionPrompt": contentExtractionPrompt,
|
||||
"extractedContent": extractedContent
|
||||
})
|
||||
|
||||
# Add to context
|
||||
context += f"\n\nDocument {len(extractedDocuments)} (from {documentReference}):\n{extractedContent}"
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error extracting content from document {chatDoc.id}: {str(e)}")
|
||||
extractedDocuments.append({
|
||||
"documentReference": documentReference,
|
||||
"documentId": chatDoc.id,
|
||||
"extractionPrompt": contentExtractionPrompt,
|
||||
"extractedContent": f"Error extracting content: {str(e)}"
|
||||
})
|
||||
else:
|
||||
logger.warning(f"No documents found for reference: {documentReference}")
|
||||
extractedDocuments.append({
|
||||
"documentReference": documentReference,
|
||||
"extractionPrompt": contentExtractionPrompt,
|
||||
"extractedContent": f"No documents found for reference: {documentReference}"
|
||||
})
|
||||
|
||||
# Create full prompt with context
|
||||
fullPrompt = f"{prompt}\n\nContext:\n{context}" if context else prompt
|
||||
|
||||
# Call AI service
|
||||
aiResponse = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(fullPrompt)
|
||||
|
||||
return {
|
||||
"prompt": prompt,
|
||||
"documentsProcessed": len(extractedDocuments),
|
||||
"extractedDocuments": extractedDocuments,
|
||||
"response": aiResponse,
|
||||
"timestamp": datetime.now(UTC).isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing AI call: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"prompt": prompt,
|
||||
"documentsProcessed": 0,
|
||||
"extractedDocuments": [],
|
||||
"response": None
|
||||
}
|
||||
|
||||
class MethodOperator(MethodBase):
|
||||
"""Operator methods for handling collections and AI operations"""
|
||||
"""Operator method implementation for handling collections and AI operations"""
|
||||
|
||||
def __init__(self, serviceContainer: Any):
|
||||
super().__init__(serviceContainer)
|
||||
self.name = "operator"
|
||||
self.description = "Operator methods for handling collections and AI operations"
|
||||
|
||||
@property
|
||||
def actions(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""Available actions and their parameters"""
|
||||
return {
|
||||
"forEach": {
|
||||
"description": "Execute an action for each item in a list",
|
||||
"parameters": {
|
||||
"items": {
|
||||
"type": "List[Any]",
|
||||
"description": "List of items to process",
|
||||
"required": True
|
||||
},
|
||||
"action": {
|
||||
"type": "Dict[str, Any]",
|
||||
"description": "Action to execute for each item",
|
||||
"required": True,
|
||||
"properties": {
|
||||
"method": {"type": "str", "required": True},
|
||||
"action": {"type": "str", "required": True},
|
||||
"parameters": {"type": "Dict[str, Any]", "required": False}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"aiCall": {
|
||||
"description": "Call AI service with document content",
|
||||
"parameters": {
|
||||
"prompt": {
|
||||
"type": "str",
|
||||
"description": "Prompt for AI processing",
|
||||
"required": True
|
||||
},
|
||||
"extractedDocumentContent": {
|
||||
"type": "List[Dict[str, str]]",
|
||||
"description": "List of documents and their extraction prompts",
|
||||
"required": True,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"document": {"type": "str", "required": True},
|
||||
"promptForContentExtraction": {"type": "str", "required": True}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async def _executeAction(self, action: str, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""Execute operator action"""
|
||||
if action == "forEach":
|
||||
return await self._executeForEach(parameters)
|
||||
elif action == "aiCall":
|
||||
return await self._executeAiCall(parameters)
|
||||
else:
|
||||
raise ValueError(f"Unsupported action: {action}")
|
||||
|
||||
async def _executeForEach(self, parameters: Dict[str, Any]) -> MethodResult:
|
||||
"""Execute forEach operation"""
|
||||
self.description = "Handle operations like forEach and AI calls"
|
||||
self.operatorService = OperatorService(serviceContainer)
|
||||
|
||||
@action
|
||||
async def forEach(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Execute an action for each item in a list"""
|
||||
try:
|
||||
items = parameters.get("items", [])
|
||||
action = parameters.get("action", {})
|
||||
|
|
@ -81,34 +159,18 @@ class MethodOperator(MethodBase):
|
|||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Missing required parameters"
|
||||
error="Items and action are required"
|
||||
)
|
||||
|
||||
results = []
|
||||
for item in items:
|
||||
try:
|
||||
# Execute action for each item
|
||||
method = action.get("method")
|
||||
action_name = action.get("action")
|
||||
action_params = action.get("parameters", {})
|
||||
|
||||
# Add current item to parameters
|
||||
action_params["item"] = item
|
||||
|
||||
# Execute method action
|
||||
method_result = await self.service.methods[method][action_name](action_params)
|
||||
results.append(method_result)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing item: {str(e)}")
|
||||
results.append({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
|
||||
# Execute forEach operation
|
||||
results = await self.operatorService.executeForEach(
|
||||
items=items,
|
||||
action=action
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={"results": results}
|
||||
data=results
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -118,55 +180,30 @@ class MethodOperator(MethodBase):
|
|||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
async def _executeAiCall(self, parameters: Dict[str, Any]) -> MethodResult:
|
||||
"""Execute AI call with document content"""
|
||||
|
||||
@action
|
||||
async def aiCall(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Call AI service with document content"""
|
||||
try:
|
||||
prompt = parameters.get("prompt")
|
||||
documents = parameters.get("extractedDocumentContent", [])
|
||||
documents = parameters.get("documents", []) # List of {documentReference, contentExtractionPrompt}
|
||||
|
||||
if not prompt:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Missing prompt parameter"
|
||||
error="Prompt is required"
|
||||
)
|
||||
|
||||
# Extract content from documents
|
||||
extracted_content = []
|
||||
for doc in documents:
|
||||
try:
|
||||
doc_ref = doc.get("document")
|
||||
doc_prompt = doc.get("promptForContentExtraction")
|
||||
|
||||
if not doc_ref or not doc_prompt:
|
||||
continue
|
||||
|
||||
# Extract content using document manager
|
||||
content = self.service.extractContent(doc_prompt, doc_ref)
|
||||
extracted_content.append({
|
||||
"document": doc_ref,
|
||||
"content": content
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting document content: {str(e)}")
|
||||
continue
|
||||
|
||||
# Prepare AI prompt with extracted content
|
||||
full_prompt = f"{prompt}\n\nExtracted Content:\n"
|
||||
for content in extracted_content:
|
||||
full_prompt += f"\nDocument: {content['document']}\n{content['content']}\n"
|
||||
|
||||
# Call AI service
|
||||
response = await self.service.callAiTextBasic(full_prompt)
|
||||
|
||||
# Execute AI call
|
||||
result = await self.operatorService.executeAiCall(
|
||||
prompt=prompt,
|
||||
documents=documents
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"response": response,
|
||||
"processedDocuments": len(extracted_content)
|
||||
}
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -5,41 +5,267 @@ Handles Outlook operations using the Outlook service.
|
|||
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from datetime import datetime, UTC
|
||||
import json
|
||||
|
||||
from modules.interfaces.interfaceOutlook import OutlookService
|
||||
from modules.methods.methodBase import MethodBase, MethodResult, action
|
||||
from modules.methods.methodBase import MethodBase, ActionResult, action
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MethodOutlook(MethodBase):
|
||||
"""Outlook method implementation"""
|
||||
class OutlookService:
|
||||
"""Service for Microsoft Outlook operations using Graph API"""
|
||||
|
||||
def __init__(self, serviceContainer):
|
||||
def __init__(self, serviceContainer: Any):
|
||||
self.serviceContainer = serviceContainer
|
||||
|
||||
def _getMicrosoftConnection(self, connectionReference: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get Microsoft connection from connection reference"""
|
||||
try:
|
||||
userConnection = self.serviceContainer.getUserConnectionFromConnectionReference(connectionReference)
|
||||
if userConnection and userConnection.authority == "microsoft" and userConnection.enabled:
|
||||
return {
|
||||
"id": userConnection.id,
|
||||
"accessToken": userConnection.accessToken,
|
||||
"refreshToken": userConnection.refreshToken,
|
||||
"scopes": userConnection.scopes
|
||||
}
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Microsoft connection: {str(e)}")
|
||||
return None
|
||||
|
||||
async def readMails(self, connectionReference: str, folder: str = "inbox", query: str = None, maxResults: int = 10, includeAttachments: bool = False) -> Dict[str, Any]:
|
||||
"""Read emails from Outlook using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate email reading
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
mail_prompt = f"""
|
||||
Read emails from Outlook.
|
||||
|
||||
Folder: {folder}
|
||||
Query: {query or 'All emails'}
|
||||
Max Results: {maxResults}
|
||||
Include Attachments: {includeAttachments}
|
||||
|
||||
Please provide:
|
||||
1. Email messages with subject, sender, and content
|
||||
2. Timestamps and priority levels
|
||||
3. Attachment information if requested
|
||||
4. Email threading and conversations
|
||||
5. Categorization and flags
|
||||
"""
|
||||
|
||||
# Use AI to simulate email data
|
||||
mail_data = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(mail_prompt)
|
||||
|
||||
return {
|
||||
"folder": folder,
|
||||
"query": query,
|
||||
"maxResults": maxResults,
|
||||
"includeAttachments": includeAttachments,
|
||||
"messages": mail_data,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading emails: {str(e)}")
|
||||
return {
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
async def sendMail(self, connectionReference: str, to: List[str], subject: str, body: str, attachments: List[str] = None) -> Dict[str, Any]:
|
||||
"""Send email using Outlook using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate email sending
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
send_prompt = f"""
|
||||
Send email using Outlook.
|
||||
|
||||
To: {', '.join(to)}
|
||||
Subject: {subject}
|
||||
Body: {body}
|
||||
Attachments: {attachments or 'None'}
|
||||
|
||||
Please provide:
|
||||
1. Email composition details
|
||||
2. Recipient validation
|
||||
3. Attachment processing
|
||||
4. Send confirmation
|
||||
5. Message tracking information
|
||||
"""
|
||||
|
||||
# Use AI to simulate email sending
|
||||
send_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(send_prompt)
|
||||
|
||||
return {
|
||||
"to": to,
|
||||
"subject": subject,
|
||||
"body": body,
|
||||
"attachments": attachments,
|
||||
"result": send_result,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending email: {str(e)}")
|
||||
return {
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
async def createFolder(self, connectionReference: str, name: str, parentFolderId: str = None) -> Dict[str, Any]:
|
||||
"""Create folder in Outlook using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate folder creation
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
folder_prompt = f"""
|
||||
Create folder in Outlook.
|
||||
|
||||
Name: {name}
|
||||
Parent Folder ID: {parentFolderId or 'Root'}
|
||||
|
||||
Please provide:
|
||||
1. Folder creation details
|
||||
2. Permission settings
|
||||
3. Folder structure and hierarchy
|
||||
4. Creation confirmation
|
||||
5. Folder properties and metadata
|
||||
"""
|
||||
|
||||
# Use AI to simulate folder creation
|
||||
folder_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(folder_prompt)
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"parentFolderId": parentFolderId,
|
||||
"result": folder_result,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating folder: {str(e)}")
|
||||
return {
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
async def moveMail(self, connectionReference: str, messageId: str, targetFolderId: str) -> Dict[str, Any]:
|
||||
"""Move email to different folder using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate mail moving
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
move_prompt = f"""
|
||||
Move email to different folder.
|
||||
|
||||
Message ID: {messageId}
|
||||
Target Folder ID: {targetFolderId}
|
||||
|
||||
Please provide:
|
||||
1. Move operation details
|
||||
2. Source and destination folder information
|
||||
3. Message preservation and metadata
|
||||
4. Move confirmation
|
||||
5. Updated folder structure
|
||||
"""
|
||||
|
||||
# Use AI to simulate mail moving
|
||||
move_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(move_prompt)
|
||||
|
||||
return {
|
||||
"messageId": messageId,
|
||||
"targetFolderId": targetFolderId,
|
||||
"result": move_result,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error moving email: {str(e)}")
|
||||
return {
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
class MethodOutlook(MethodBase):
|
||||
"""Outlook method implementation for email operations"""
|
||||
|
||||
def __init__(self, serviceContainer: Any):
|
||||
"""Initialize the Outlook method"""
|
||||
super().__init__(serviceContainer)
|
||||
self.name = "outlook"
|
||||
self.description = "Handle Outlook email operations like reading and sending emails"
|
||||
self.outlookService = OutlookService(serviceContainer)
|
||||
|
||||
@action
|
||||
async def readMails(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
async def readMails(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""
|
||||
Read emails from Outlook
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
connectionReference: Connection reference
|
||||
folder: Folder to read from (default: inbox)
|
||||
query: Search query
|
||||
maxResults: Maximum number of results
|
||||
includeAttachments: Whether to include attachments
|
||||
"""
|
||||
try:
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
folder = parameters.get("folder", "inbox")
|
||||
query = parameters.get("query")
|
||||
maxResults = parameters.get("maxResults", 10)
|
||||
includeAttachments = parameters.get("includeAttachments", False)
|
||||
|
||||
if not connectionReference:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Connection reference is required"
|
||||
)
|
||||
|
||||
# Read emails
|
||||
emails = await self.outlookService.readEmails(
|
||||
messages = await self.outlookService.readMails(
|
||||
connectionReference=connectionReference,
|
||||
folder=folder,
|
||||
query=query,
|
||||
maxResults=maxResults,
|
||||
|
|
@ -48,40 +274,54 @@ class MethodOutlook(MethodBase):
|
|||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"folder": folder,
|
||||
"query": query,
|
||||
"emails": emails
|
||||
}
|
||||
data=messages
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading emails: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def sendMail(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
async def sendMail(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""
|
||||
Send email using Outlook
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
connectionReference: Connection reference
|
||||
to: List of recipient email addresses
|
||||
subject: Email subject
|
||||
body: Email body
|
||||
attachments: List of attachment file IDs
|
||||
"""
|
||||
try:
|
||||
to = parameters["to"]
|
||||
subject = parameters["subject"]
|
||||
body = parameters["body"]
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
to = parameters.get("to", [])
|
||||
subject = parameters.get("subject")
|
||||
body = parameters.get("body")
|
||||
attachments = parameters.get("attachments", [])
|
||||
|
||||
if not connectionReference:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Connection reference is required"
|
||||
)
|
||||
|
||||
if not to or not subject or not body:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="To, subject, and body are required"
|
||||
)
|
||||
|
||||
# Send email
|
||||
messageId = await self.outlookService.sendEmail(
|
||||
result = await self.outlookService.sendMail(
|
||||
connectionReference=connectionReference,
|
||||
to=to,
|
||||
subject=subject,
|
||||
body=body,
|
||||
|
|
@ -90,87 +330,113 @@ class MethodOutlook(MethodBase):
|
|||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"messageId": messageId,
|
||||
"to": to,
|
||||
"subject": subject
|
||||
}
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending email: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def createFolder(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
async def createFolder(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""
|
||||
Create folder in Outlook
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
connectionReference: Connection reference
|
||||
name: Folder name
|
||||
parentFolder: Parent folder ID (optional)
|
||||
parentFolderId: Parent folder ID (optional)
|
||||
"""
|
||||
try:
|
||||
name = parameters["name"]
|
||||
parentFolder = parameters.get("parentFolder")
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
name = parameters.get("name")
|
||||
parentFolderId = parameters.get("parentFolderId")
|
||||
|
||||
if not connectionReference:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Connection reference is required"
|
||||
)
|
||||
|
||||
if not name:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Folder name is required"
|
||||
)
|
||||
|
||||
# Create folder
|
||||
folderId = await self.outlookService.createFolder(
|
||||
folder = await self.outlookService.createFolder(
|
||||
connectionReference=connectionReference,
|
||||
name=name,
|
||||
parentFolder=parentFolder
|
||||
parentFolderId=parentFolderId
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"folderId": folderId,
|
||||
"name": name,
|
||||
"parentFolder": parentFolder
|
||||
}
|
||||
data=folder
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating folder: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def moveMail(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
async def moveMail(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""
|
||||
Move email to different folder
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
connectionReference: Connection reference
|
||||
messageId: ID of the message to move
|
||||
targetFolder: ID of the target folder
|
||||
targetFolderId: ID of the target folder
|
||||
"""
|
||||
try:
|
||||
messageId = parameters["messageId"]
|
||||
targetFolder = parameters["targetFolder"]
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
messageId = parameters.get("messageId")
|
||||
targetFolderId = parameters.get("targetFolderId")
|
||||
|
||||
if not connectionReference:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Connection reference is required"
|
||||
)
|
||||
|
||||
if not messageId or not targetFolderId:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Message ID and target folder ID are required"
|
||||
)
|
||||
|
||||
# Move email
|
||||
await self.outlookService.moveEmail(
|
||||
result = await self.outlookService.moveMail(
|
||||
connectionReference=connectionReference,
|
||||
messageId=messageId,
|
||||
targetFolder=targetFolder
|
||||
targetFolderId=targetFolderId
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"messageId": messageId,
|
||||
"targetFolder": targetFolder
|
||||
}
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error moving email: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
|
@ -5,256 +5,584 @@ Handles PowerPoint operations using the PowerPoint service.
|
|||
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from datetime import datetime, UTC
|
||||
import json
|
||||
import base64
|
||||
|
||||
from modules.interfaces.interfacePowerpoint import PowerpointService
|
||||
from modules.methods.methodBase import MethodBase, MethodResult, action
|
||||
from modules.methods.methodBase import MethodBase, ActionResult, action
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MethodPowerpoint(MethodBase):
|
||||
"""PowerPoint method implementation"""
|
||||
class PowerpointService:
|
||||
"""Service for Microsoft PowerPoint operations using Graph API"""
|
||||
|
||||
def __init__(self, serviceContainer):
|
||||
def __init__(self, serviceContainer: Any):
|
||||
self.serviceContainer = serviceContainer
|
||||
|
||||
def _getMicrosoftConnection(self, connectionReference: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get Microsoft connection from connection reference"""
|
||||
try:
|
||||
userConnection = self.serviceContainer.getUserConnectionFromConnectionReference(connectionReference)
|
||||
if userConnection and userConnection.authority == "microsoft" and userConnection.enabled:
|
||||
return {
|
||||
"id": userConnection.id,
|
||||
"accessToken": userConnection.accessToken,
|
||||
"refreshToken": userConnection.refreshToken,
|
||||
"scopes": userConnection.scopes
|
||||
}
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Microsoft connection: {str(e)}")
|
||||
return None
|
||||
|
||||
async def readPresentation(self, fileId: str, connectionReference: str, includeSlides: bool = True) -> Dict[str, Any]:
|
||||
"""Read PowerPoint presentation using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"fileId": fileId,
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# Get file data from service container
|
||||
file_data = self.serviceContainer.getFileData(fileId)
|
||||
file_info = self.serviceContainer.getFileInfo(fileId)
|
||||
|
||||
if not file_data:
|
||||
return {
|
||||
"error": "File not found or empty",
|
||||
"fileId": fileId
|
||||
}
|
||||
|
||||
# For now, simulate PowerPoint reading with AI analysis
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
ppt_prompt = f"""
|
||||
Analyze this PowerPoint presentation and extract structured information.
|
||||
|
||||
File: {file_info.get('name', 'Unknown')}
|
||||
Include slides: {includeSlides}
|
||||
|
||||
File content (first 5000 characters):
|
||||
{file_data.decode('utf-8', errors='ignore')[:5000] if isinstance(file_data, bytes) else str(file_data)[:5000]}
|
||||
|
||||
Please extract:
|
||||
1. Presentation title and theme
|
||||
2. Slide structure and content
|
||||
3. Text content from each slide
|
||||
4. Images and media references
|
||||
5. Charts and data visualizations
|
||||
6. Speaker notes if available
|
||||
7. Overall presentation flow and messaging
|
||||
|
||||
Return the data in a structured JSON format.
|
||||
"""
|
||||
|
||||
# Use AI to analyze PowerPoint content
|
||||
analysis_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(ppt_prompt)
|
||||
|
||||
return {
|
||||
"fileId": fileId,
|
||||
"includeSlides": includeSlides,
|
||||
"data": analysis_result,
|
||||
"fileInfo": file_info,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading presentation: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"fileId": fileId
|
||||
}
|
||||
|
||||
async def writePresentation(self, fileId: str, connectionReference: str, slides: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""Write to PowerPoint presentation using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"fileId": fileId,
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate PowerPoint writing
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
write_prompt = f"""
|
||||
Prepare content for writing to PowerPoint presentation.
|
||||
|
||||
File: {fileId}
|
||||
Number of slides: {len(slides)}
|
||||
|
||||
Slides data:
|
||||
{json.dumps(slides, indent=2)}
|
||||
|
||||
Please format this content appropriately for PowerPoint and provide:
|
||||
1. Slide layouts and structures
|
||||
2. Text content and formatting
|
||||
3. Image and media placement
|
||||
4. Chart and visualization specifications
|
||||
5. Animation and transition suggestions
|
||||
"""
|
||||
|
||||
# Use AI to prepare PowerPoint content
|
||||
prepared_content = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(write_prompt)
|
||||
|
||||
return {
|
||||
"fileId": fileId,
|
||||
"slides": slides,
|
||||
"content": prepared_content,
|
||||
"status": "prepared",
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error writing to presentation: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"fileId": fileId
|
||||
}
|
||||
|
||||
async def convertPresentation(self, fileId: str, connectionReference: str, format: str = "pdf") -> Dict[str, Any]:
|
||||
"""Convert PowerPoint presentation to another format using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"fileId": fileId,
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate conversion
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
convert_prompt = f"""
|
||||
Convert PowerPoint presentation to {format.upper()} format.
|
||||
|
||||
File: {fileId}
|
||||
Target format: {format}
|
||||
|
||||
Please provide:
|
||||
1. Conversion specifications
|
||||
2. Format-specific optimizations
|
||||
3. Quality settings and options
|
||||
4. Any special considerations for the target format
|
||||
"""
|
||||
|
||||
# Use AI to describe conversion process
|
||||
conversion_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(convert_prompt)
|
||||
|
||||
# Create converted file using service container
|
||||
converted_file_id = self.serviceContainer.createFile(
|
||||
fileName=f"converted_presentation.{format}",
|
||||
mimeType=f"application/{format}",
|
||||
content=conversion_result,
|
||||
base64encoded=False
|
||||
)
|
||||
|
||||
return {
|
||||
"fileId": fileId,
|
||||
"format": format,
|
||||
"convertedFileId": converted_file_id,
|
||||
"result": conversion_result,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error converting presentation: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"fileId": fileId
|
||||
}
|
||||
|
||||
async def createPresentation(self, fileName: str, connectionReference: str, template: str = None) -> Dict[str, Any]:
|
||||
"""Create new PowerPoint presentation using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate presentation creation
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
create_prompt = f"""
|
||||
Create a new PowerPoint presentation structure.
|
||||
|
||||
File name: {fileName}
|
||||
Template: {template or 'Standard'}
|
||||
|
||||
Please provide:
|
||||
1. Initial slide structure
|
||||
2. Default slide layouts
|
||||
3. Theme and design elements
|
||||
4. Sample content if template specified
|
||||
5. Presentation guidelines
|
||||
"""
|
||||
|
||||
# Use AI to create PowerPoint structure
|
||||
presentation_structure = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(create_prompt)
|
||||
|
||||
# Create file using service container
|
||||
file_id = self.serviceContainer.createFile(
|
||||
fileName=fileName,
|
||||
mimeType="application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
content=presentation_structure,
|
||||
base64encoded=False
|
||||
)
|
||||
|
||||
return {
|
||||
"fileId": file_id,
|
||||
"fileName": fileName,
|
||||
"template": template,
|
||||
"structure": presentation_structure,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating presentation: {str(e)}")
|
||||
return {
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
async def addSlide(self, fileId: str, connectionReference: str, layout: str = "title", content: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
"""Add slide to presentation using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"fileId": fileId,
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate slide addition
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
slide_prompt = f"""
|
||||
Add a new slide to PowerPoint presentation.
|
||||
|
||||
File: {fileId}
|
||||
Layout: {layout}
|
||||
Content: {json.dumps(content, indent=2) if content else 'Default content'}
|
||||
|
||||
Please provide:
|
||||
1. Slide structure and layout
|
||||
2. Content placement and formatting
|
||||
3. Visual elements and design
|
||||
4. Slide number and positioning
|
||||
"""
|
||||
|
||||
# Use AI to create slide content
|
||||
slide_content = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(slide_prompt)
|
||||
|
||||
return {
|
||||
"fileId": fileId,
|
||||
"layout": layout,
|
||||
"content": content,
|
||||
"slideContent": slide_content,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding slide: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"fileId": fileId
|
||||
}
|
||||
|
||||
async def addContent(self, fileId: str, connectionReference: str, slideId: str, content: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add content to slide using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"fileId": fileId,
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate content addition
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
content_prompt = f"""
|
||||
Add content to PowerPoint slide.
|
||||
|
||||
File: {fileId}
|
||||
Slide ID: {slideId}
|
||||
Content: {json.dumps(content, indent=2)}
|
||||
|
||||
Please provide:
|
||||
1. Content placement and formatting
|
||||
2. Text styling and layout
|
||||
3. Image and media integration
|
||||
4. Chart and visualization setup
|
||||
5. Animation and effects
|
||||
"""
|
||||
|
||||
# Use AI to format slide content
|
||||
formatted_content = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(content_prompt)
|
||||
|
||||
return {
|
||||
"fileId": fileId,
|
||||
"slideId": slideId,
|
||||
"content": content,
|
||||
"formattedContent": formatted_content,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding content: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"fileId": fileId
|
||||
}
|
||||
|
||||
class MethodPowerpoint(MethodBase):
|
||||
"""PowerPoint method implementation for presentation operations"""
|
||||
|
||||
def __init__(self, serviceContainer: Any):
|
||||
"""Initialize the PowerPoint method"""
|
||||
super().__init__(serviceContainer)
|
||||
self.name = "powerpoint"
|
||||
self.description = "Handle PowerPoint presentation operations like reading and creating slides"
|
||||
self.powerpointService = PowerpointService(serviceContainer)
|
||||
|
||||
@action
|
||||
async def read(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Read PowerPoint presentation
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
fileId: ID of the PowerPoint file
|
||||
includeSlides: Whether to include slide content
|
||||
"""
|
||||
async def read(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Read PowerPoint presentation"""
|
||||
try:
|
||||
fileId = parameters["fileId"]
|
||||
fileId = parameters.get("fileId")
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
includeSlides = parameters.get("includeSlides", True)
|
||||
|
||||
# Get file from service
|
||||
file = await self.service.interfaceComponent.getFile(fileId)
|
||||
if not file:
|
||||
if not fileId or not connectionReference:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"File not found: {fileId}"}
|
||||
data={},
|
||||
error="File ID and connection reference are required"
|
||||
)
|
||||
|
||||
# Read presentation
|
||||
presentation = await self.powerpointService.readPresentation(file, includeSlides)
|
||||
data = await self.powerpointService.readPresentation(
|
||||
fileId=fileId,
|
||||
connectionReference=connectionReference,
|
||||
includeSlides=includeSlides
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"fileId": fileId,
|
||||
"presentation": presentation
|
||||
}
|
||||
data=data
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading PowerPoint: {str(e)}")
|
||||
logger.error(f"Error reading presentation: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def write(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Write PowerPoint presentation
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
fileId: ID of the PowerPoint file
|
||||
slides: List of slide configurations
|
||||
"""
|
||||
async def write(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Write to PowerPoint presentation"""
|
||||
try:
|
||||
fileId = parameters["fileId"]
|
||||
slides = parameters["slides"]
|
||||
fileId = parameters.get("fileId")
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
slides = parameters.get("slides", [])
|
||||
|
||||
# Get file from service
|
||||
file = await self.service.interfaceComponent.getFile(fileId)
|
||||
if not file:
|
||||
if not fileId or not connectionReference:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"File not found: {fileId}"}
|
||||
data={},
|
||||
error="File ID and connection reference are required"
|
||||
)
|
||||
|
||||
# Write presentation
|
||||
await self.powerpointService.writePresentation(file, slides)
|
||||
# Write to presentation
|
||||
result = await self.powerpointService.writePresentation(
|
||||
fileId=fileId,
|
||||
connectionReference=connectionReference,
|
||||
slides=slides
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"fileId": fileId,
|
||||
"slideCount": len(slides)
|
||||
}
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error writing PowerPoint: {str(e)}")
|
||||
logger.error(f"Error writing to presentation: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def convert(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Convert PowerPoint to other format
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
fileId: ID of the PowerPoint file
|
||||
format: Target format (pdf, png, etc.)
|
||||
"""
|
||||
async def convert(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Convert PowerPoint presentation to another format"""
|
||||
try:
|
||||
fileId = parameters["fileId"]
|
||||
format = parameters["format"]
|
||||
fileId = parameters.get("fileId")
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
format = parameters.get("format", "pdf")
|
||||
|
||||
# Get file from service
|
||||
file = await self.service.interfaceComponent.getFile(fileId)
|
||||
if not file:
|
||||
if not fileId or not connectionReference:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"File not found: {fileId}"}
|
||||
data={},
|
||||
error="File ID and connection reference are required"
|
||||
)
|
||||
|
||||
# Convert presentation
|
||||
convertedFile = await self.powerpointService.convertPresentation(file, format)
|
||||
result = await self.powerpointService.convertPresentation(
|
||||
fileId=fileId,
|
||||
connectionReference=connectionReference,
|
||||
format=format
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"fileId": fileId,
|
||||
"format": format,
|
||||
"convertedFileId": convertedFile.id
|
||||
}
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error converting PowerPoint: {str(e)}")
|
||||
logger.error(f"Error converting presentation: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def createPresentation(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Create new PowerPoint presentation
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
fileName: Name of the new file
|
||||
template: Template ID (optional)
|
||||
"""
|
||||
async def createPresentation(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Create new PowerPoint presentation"""
|
||||
try:
|
||||
fileName = parameters["fileName"]
|
||||
fileName = parameters.get("fileName")
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
template = parameters.get("template")
|
||||
|
||||
if not fileName or not connectionReference:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="File name and connection reference are required"
|
||||
)
|
||||
|
||||
# Create presentation
|
||||
file = await self.powerpointService.createPresentation(fileName, template)
|
||||
fileId = await self.powerpointService.createPresentation(
|
||||
fileName=fileName,
|
||||
connectionReference=connectionReference,
|
||||
template=template
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"fileId": file.id,
|
||||
"fileName": fileName,
|
||||
"template": template
|
||||
}
|
||||
data={"fileId": fileId}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating PowerPoint: {str(e)}")
|
||||
logger.error(f"Error creating presentation: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def addSlide(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Add slide to presentation
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
fileId: ID of the PowerPoint file
|
||||
layout: Slide layout
|
||||
content: Slide content
|
||||
"""
|
||||
async def addSlide(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Add slide to presentation"""
|
||||
try:
|
||||
fileId = parameters["fileId"]
|
||||
fileId = parameters.get("fileId")
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
layout = parameters.get("layout", "title")
|
||||
content = parameters.get("content", {})
|
||||
|
||||
# Get file from service
|
||||
file = await self.service.interfaceComponent.getFile(fileId)
|
||||
if not file:
|
||||
if not fileId or not connectionReference:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"File not found: {fileId}"}
|
||||
data={},
|
||||
error="File ID and connection reference are required"
|
||||
)
|
||||
|
||||
# Add slide
|
||||
slideId = await self.powerpointService.addSlide(file, layout, content)
|
||||
slide = await self.powerpointService.addSlide(
|
||||
fileId=fileId,
|
||||
connectionReference=connectionReference,
|
||||
layout=layout,
|
||||
content=content
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"fileId": fileId,
|
||||
"slideId": slideId,
|
||||
"layout": layout
|
||||
}
|
||||
data=slide
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding slide: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def addContent(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Add content to slide
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
fileId: ID of the PowerPoint file
|
||||
slideId: ID of the slide
|
||||
content: Content to add
|
||||
"""
|
||||
async def addContent(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Add content to slide"""
|
||||
try:
|
||||
fileId = parameters["fileId"]
|
||||
slideId = parameters["slideId"]
|
||||
content = parameters["content"]
|
||||
fileId = parameters.get("fileId")
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
slideId = parameters.get("slideId")
|
||||
content = parameters.get("content", {})
|
||||
|
||||
# Get file from service
|
||||
file = await self.service.interfaceComponent.getFile(fileId)
|
||||
if not file:
|
||||
if not fileId or not connectionReference or not slideId:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": f"File not found: {fileId}"}
|
||||
data={},
|
||||
error="File ID, connection reference, and slide ID are required"
|
||||
)
|
||||
|
||||
# Add content
|
||||
await self.powerpointService.addContent(file, slideId, content)
|
||||
result = await self.powerpointService.addContent(
|
||||
fileId=fileId,
|
||||
connectionReference=connectionReference,
|
||||
slideId=slideId,
|
||||
content=content
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"fileId": fileId,
|
||||
"slideId": slideId
|
||||
}
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding content: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
|
@ -5,41 +5,373 @@ Handles SharePoint operations using the SharePoint service.
|
|||
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from datetime import datetime, UTC
|
||||
import json
|
||||
|
||||
from modules.interfaces.interfaceSharepoint import SharepointService
|
||||
from modules.methods.methodBase import MethodBase, MethodResult, action
|
||||
from modules.methods.methodBase import MethodBase, ActionResult, action
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MethodSharepoint(MethodBase):
|
||||
"""SharePoint method implementation"""
|
||||
class SharepointService:
|
||||
"""Service for Microsoft SharePoint operations using Graph API"""
|
||||
|
||||
def __init__(self, serviceContainer):
|
||||
def __init__(self, serviceContainer: Any):
|
||||
self.serviceContainer = serviceContainer
|
||||
|
||||
def _getMicrosoftConnection(self, connectionReference: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get Microsoft connection from connection reference"""
|
||||
try:
|
||||
userConnection = self.serviceContainer.getUserConnectionFromConnectionReference(connectionReference)
|
||||
if userConnection and userConnection.authority == "microsoft" and userConnection.enabled:
|
||||
return {
|
||||
"id": userConnection.id,
|
||||
"accessToken": userConnection.accessToken,
|
||||
"refreshToken": userConnection.refreshToken,
|
||||
"scopes": userConnection.scopes
|
||||
}
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Microsoft connection: {str(e)}")
|
||||
return None
|
||||
|
||||
async def searchContent(self, connectionReference: str, query: str, siteId: str = None, contentType: str = None, maxResults: int = 10) -> Dict[str, Any]:
|
||||
"""Search SharePoint content using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate SharePoint search
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
search_prompt = f"""
|
||||
Search SharePoint content for the following query.
|
||||
|
||||
Query: {query}
|
||||
Site ID: {siteId or 'All sites'}
|
||||
Content Type: {contentType or 'All types'}
|
||||
Max Results: {maxResults}
|
||||
|
||||
Please provide:
|
||||
1. Relevant search results
|
||||
2. Content summaries
|
||||
3. File and document information
|
||||
4. Site and list references
|
||||
5. Metadata and properties
|
||||
"""
|
||||
|
||||
# Use AI to simulate search results
|
||||
search_results = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(search_prompt)
|
||||
|
||||
return {
|
||||
"query": query,
|
||||
"siteId": siteId,
|
||||
"contentType": contentType,
|
||||
"maxResults": maxResults,
|
||||
"results": search_results,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching SharePoint: {str(e)}")
|
||||
return {
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
async def readItem(self, connectionReference: str, itemId: str, siteId: str = None, listId: str = None) -> Dict[str, Any]:
|
||||
"""Read SharePoint item using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"itemId": itemId,
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate item reading
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
read_prompt = f"""
|
||||
Read SharePoint item details.
|
||||
|
||||
Item ID: {itemId}
|
||||
Site ID: {siteId or 'Default site'}
|
||||
List ID: {listId or 'Default list'}
|
||||
|
||||
Please provide:
|
||||
1. Item properties and metadata
|
||||
2. Content and attachments
|
||||
3. Permissions and access rights
|
||||
4. Version history if available
|
||||
5. Related items and links
|
||||
"""
|
||||
|
||||
# Use AI to simulate item data
|
||||
item_data = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(read_prompt)
|
||||
|
||||
return {
|
||||
"itemId": itemId,
|
||||
"siteId": siteId,
|
||||
"listId": listId,
|
||||
"data": item_data,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading SharePoint item: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"itemId": itemId
|
||||
}
|
||||
|
||||
async def writeItem(self, connectionReference: str, siteId: str, listId: str, item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Write SharePoint item using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate item writing
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
write_prompt = f"""
|
||||
Write item to SharePoint list.
|
||||
|
||||
Site ID: {siteId}
|
||||
List ID: {listId}
|
||||
Item data: {json.dumps(item, indent=2)}
|
||||
|
||||
Please provide:
|
||||
1. Item creation/update details
|
||||
2. Validation and formatting
|
||||
3. Permission settings
|
||||
4. Workflow triggers if applicable
|
||||
5. Success confirmation
|
||||
"""
|
||||
|
||||
# Use AI to simulate item creation
|
||||
write_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(write_prompt)
|
||||
|
||||
return {
|
||||
"siteId": siteId,
|
||||
"listId": listId,
|
||||
"item": item,
|
||||
"result": write_result,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error writing SharePoint item: {str(e)}")
|
||||
return {
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
async def readList(self, connectionReference: str, listId: str, siteId: str = None, query: str = None, maxResults: int = 10) -> Dict[str, Any]:
|
||||
"""Read SharePoint list using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"listId": listId,
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate list reading
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
list_prompt = f"""
|
||||
Read SharePoint list items.
|
||||
|
||||
List ID: {listId}
|
||||
Site ID: {siteId or 'Default site'}
|
||||
Query: {query or 'All items'}
|
||||
Max Results: {maxResults}
|
||||
|
||||
Please provide:
|
||||
1. List structure and columns
|
||||
2. Item data and properties
|
||||
3. Sorting and filtering options
|
||||
4. Pagination information
|
||||
5. List metadata and settings
|
||||
"""
|
||||
|
||||
# Use AI to simulate list data
|
||||
list_data = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(list_prompt)
|
||||
|
||||
return {
|
||||
"listId": listId,
|
||||
"siteId": siteId,
|
||||
"query": query,
|
||||
"maxResults": maxResults,
|
||||
"data": list_data,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading SharePoint list: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"listId": listId
|
||||
}
|
||||
|
||||
async def writeList(self, connectionReference: str, siteId: str, listId: str, items: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""Write multiple items to SharePoint list using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate bulk writing
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
bulk_prompt = f"""
|
||||
Write multiple items to SharePoint list.
|
||||
|
||||
Site ID: {siteId}
|
||||
List ID: {listId}
|
||||
Number of items: {len(items)}
|
||||
Items data: {json.dumps(items[:3], indent=2)} # Show first 3 items
|
||||
|
||||
Please provide:
|
||||
1. Bulk operation details
|
||||
2. Validation and error handling
|
||||
3. Performance optimization
|
||||
4. Success/failure status for each item
|
||||
5. Batch processing results
|
||||
"""
|
||||
|
||||
# Use AI to simulate bulk operation
|
||||
bulk_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(bulk_prompt)
|
||||
|
||||
return {
|
||||
"siteId": siteId,
|
||||
"listId": listId,
|
||||
"items": items,
|
||||
"result": bulk_result,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error writing to SharePoint list: {str(e)}")
|
||||
return {
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
async def createList(self, connectionReference: str, siteId: str, name: str, description: str = None, template: str = "genericList", fields: List[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""Create SharePoint list using Microsoft Graph API"""
|
||||
try:
|
||||
connection = self._getMicrosoftConnection(connectionReference)
|
||||
if not connection:
|
||||
return {
|
||||
"error": "No valid Microsoft connection found for the provided connection reference",
|
||||
"connectionReference": connectionReference
|
||||
}
|
||||
|
||||
# For now, simulate list creation
|
||||
# In a real implementation, you would use Microsoft Graph API
|
||||
create_prompt = f"""
|
||||
Create a new SharePoint list.
|
||||
|
||||
Site ID: {siteId}
|
||||
Name: {name}
|
||||
Description: {description or 'No description'}
|
||||
Template: {template}
|
||||
Fields: {json.dumps(fields, indent=2) if fields else 'Default fields'}
|
||||
|
||||
Please provide:
|
||||
1. List structure and configuration
|
||||
2. Column definitions and types
|
||||
3. Default views and permissions
|
||||
4. Workflow and automation settings
|
||||
5. Creation confirmation and next steps
|
||||
"""
|
||||
|
||||
# Use AI to simulate list creation
|
||||
creation_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(create_prompt)
|
||||
|
||||
return {
|
||||
"siteId": siteId,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"template": template,
|
||||
"fields": fields,
|
||||
"result": creation_result,
|
||||
"connection": {
|
||||
"id": connection["id"],
|
||||
"authority": "microsoft",
|
||||
"reference": connectionReference
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating SharePoint list: {str(e)}")
|
||||
return {
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
class MethodSharepoint(MethodBase):
|
||||
"""SharePoint method implementation for site operations"""
|
||||
|
||||
def __init__(self, serviceContainer: Any):
|
||||
"""Initialize the SharePoint method"""
|
||||
super().__init__(serviceContainer)
|
||||
self.name = "sharepoint"
|
||||
self.description = "Handle SharePoint site operations like reading and writing lists"
|
||||
self.sharepointService = SharepointService(serviceContainer)
|
||||
|
||||
@action
|
||||
async def search(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Search SharePoint content
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
query: Search query
|
||||
siteId: Site ID to search in
|
||||
contentType: Content type to search for
|
||||
maxResults: Maximum number of results
|
||||
"""
|
||||
async def search(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Search SharePoint content"""
|
||||
try:
|
||||
query = parameters["query"]
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
query = parameters.get("query")
|
||||
siteId = parameters.get("siteId")
|
||||
contentType = parameters.get("contentType")
|
||||
maxResults = parameters.get("maxResults", 10)
|
||||
|
||||
if not connectionReference:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Connection reference is required"
|
||||
)
|
||||
|
||||
if not query:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Search query is required"
|
||||
)
|
||||
|
||||
# Search content
|
||||
results = await self.sharepointService.searchContent(
|
||||
connectionReference=connectionReference,
|
||||
query=query,
|
||||
siteId=siteId,
|
||||
contentType=contentType,
|
||||
|
|
@ -48,39 +380,43 @@ class MethodSharepoint(MethodBase):
|
|||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"query": query,
|
||||
"siteId": siteId,
|
||||
"contentType": contentType,
|
||||
"results": results
|
||||
}
|
||||
data=results
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching SharePoint: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def read(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Read SharePoint item
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
itemId: ID of the item to read
|
||||
siteId: Site ID containing the item
|
||||
listId: List ID containing the item
|
||||
"""
|
||||
async def read(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Read SharePoint item"""
|
||||
try:
|
||||
itemId = parameters["itemId"]
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
itemId = parameters.get("itemId")
|
||||
siteId = parameters.get("siteId")
|
||||
listId = parameters.get("listId")
|
||||
|
||||
if not connectionReference:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Connection reference is required"
|
||||
)
|
||||
|
||||
if not itemId:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Item ID is required"
|
||||
)
|
||||
|
||||
# Read item
|
||||
item = await self.sharepointService.readItem(
|
||||
connectionReference=connectionReference,
|
||||
itemId=itemId,
|
||||
siteId=siteId,
|
||||
listId=listId
|
||||
|
|
@ -88,39 +424,43 @@ class MethodSharepoint(MethodBase):
|
|||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"itemId": itemId,
|
||||
"siteId": siteId,
|
||||
"listId": listId,
|
||||
"item": item
|
||||
}
|
||||
data=item
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading SharePoint item: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def write(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Write SharePoint item
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
siteId: Site ID to write to
|
||||
listId: List ID to write to
|
||||
item: Item data to write
|
||||
"""
|
||||
async def write(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Write SharePoint item"""
|
||||
try:
|
||||
siteId = parameters["siteId"]
|
||||
listId = parameters["listId"]
|
||||
item = parameters["item"]
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
siteId = parameters.get("siteId")
|
||||
listId = parameters.get("listId")
|
||||
item = parameters.get("item", {})
|
||||
|
||||
if not connectionReference:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Connection reference is required"
|
||||
)
|
||||
|
||||
if not siteId or not listId:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Site ID and list ID are required"
|
||||
)
|
||||
|
||||
# Write item
|
||||
itemId = await self.sharepointService.writeItem(
|
||||
result = await self.sharepointService.writeItem(
|
||||
connectionReference=connectionReference,
|
||||
siteId=siteId,
|
||||
listId=listId,
|
||||
item=item
|
||||
|
|
@ -128,40 +468,44 @@ class MethodSharepoint(MethodBase):
|
|||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"siteId": siteId,
|
||||
"listId": listId,
|
||||
"itemId": itemId
|
||||
}
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error writing SharePoint item: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def readList(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Read SharePoint list
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
listId: ID of the list to read
|
||||
siteId: Site ID containing the list
|
||||
query: Query to filter items
|
||||
maxResults: Maximum number of results
|
||||
"""
|
||||
async def readList(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Read SharePoint list"""
|
||||
try:
|
||||
listId = parameters["listId"]
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
listId = parameters.get("listId")
|
||||
siteId = parameters.get("siteId")
|
||||
query = parameters.get("query")
|
||||
maxResults = parameters.get("maxResults", 100)
|
||||
maxResults = parameters.get("maxResults", 10)
|
||||
|
||||
if not connectionReference:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Connection reference is required"
|
||||
)
|
||||
|
||||
if not listId:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="List ID is required"
|
||||
)
|
||||
|
||||
# Read list
|
||||
items = await self.sharepointService.readList(
|
||||
connectionReference=connectionReference,
|
||||
listId=listId,
|
||||
siteId=siteId,
|
||||
query=query,
|
||||
|
|
@ -170,38 +514,43 @@ class MethodSharepoint(MethodBase):
|
|||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"listId": listId,
|
||||
"siteId": siteId,
|
||||
"items": items
|
||||
}
|
||||
data=items
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading SharePoint list: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def writeList(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Write multiple items to SharePoint list
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
siteId: Site ID to write to
|
||||
listId: List ID to write to
|
||||
items: List of item data to write
|
||||
"""
|
||||
async def writeList(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Write multiple items to SharePoint list"""
|
||||
try:
|
||||
siteId = parameters["siteId"]
|
||||
listId = parameters["listId"]
|
||||
items = parameters["items"]
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
siteId = parameters.get("siteId")
|
||||
listId = parameters.get("listId")
|
||||
items = parameters.get("items", [])
|
||||
|
||||
if not connectionReference:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Connection reference is required"
|
||||
)
|
||||
|
||||
if not siteId or not listId:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Site ID and list ID are required"
|
||||
)
|
||||
|
||||
# Write items
|
||||
itemIds = await self.sharepointService.writeList(
|
||||
result = await self.sharepointService.writeList(
|
||||
connectionReference=connectionReference,
|
||||
siteId=siteId,
|
||||
listId=listId,
|
||||
items=items
|
||||
|
|
@ -209,42 +558,45 @@ class MethodSharepoint(MethodBase):
|
|||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"siteId": siteId,
|
||||
"listId": listId,
|
||||
"itemIds": itemIds
|
||||
}
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error writing SharePoint list: {str(e)}")
|
||||
logger.error(f"Error writing to SharePoint list: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def createList(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Create SharePoint list
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
siteId: Site ID to create list in
|
||||
name: Name of the list
|
||||
description: List description
|
||||
template: List template
|
||||
fields: List field definitions
|
||||
"""
|
||||
async def createList(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Create SharePoint list"""
|
||||
try:
|
||||
siteId = parameters["siteId"]
|
||||
name = parameters["name"]
|
||||
connectionReference = parameters.get("connectionReference")
|
||||
siteId = parameters.get("siteId")
|
||||
name = parameters.get("name")
|
||||
description = parameters.get("description")
|
||||
template = parameters.get("template", "genericList")
|
||||
fields = parameters.get("fields", [])
|
||||
|
||||
if not connectionReference:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Connection reference is required"
|
||||
)
|
||||
|
||||
if not siteId or not name:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Site ID and list name are required"
|
||||
)
|
||||
|
||||
# Create list
|
||||
listId = await self.sharepointService.createList(
|
||||
list = await self.sharepointService.createList(
|
||||
connectionReference=connectionReference,
|
||||
siteId=siteId,
|
||||
name=name,
|
||||
description=description,
|
||||
|
|
@ -254,16 +606,13 @@ class MethodSharepoint(MethodBase):
|
|||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"siteId": siteId,
|
||||
"listId": listId,
|
||||
"name": name
|
||||
}
|
||||
data=list
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating SharePoint list: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
|
@ -5,78 +5,496 @@ Handles web operations using the web service.
|
|||
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from datetime import datetime, UTC
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import time
|
||||
|
||||
from modules.interfaces.interfaceWeb import WebService
|
||||
from modules.methods.methodBase import MethodBase, MethodResult, action
|
||||
from modules.methods.methodBase import MethodBase, ActionResult, action
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MethodWeb(MethodBase):
|
||||
"""Web method implementation"""
|
||||
class WebService:
|
||||
"""Service for web operations like searching and crawling"""
|
||||
|
||||
def __init__(self, serviceContainer):
|
||||
def __init__(self, serviceContainer: Any):
|
||||
self.serviceContainer = serviceContainer
|
||||
self.user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
self.timeout = 30
|
||||
|
||||
# Web search configuration from agentWebcrawler
|
||||
self.srcApikey = APP_CONFIG.get("Agent_Webcrawler_SERPAPI_APIKEY", "")
|
||||
self.srcEngine = APP_CONFIG.get("Agent_Webcrawler_SERPAPI_ENGINE", "google")
|
||||
self.srcCountry = APP_CONFIG.get("Agent_Webcrawler_SERPAPI_COUNTRY", "auto")
|
||||
self.maxResults = int(APP_CONFIG.get("Agent_Webcrawler_SERPAPI_MAX_SEARCH_RESULTS", "5"))
|
||||
|
||||
if not self.srcApikey:
|
||||
logger.warning("SerpAPI key not configured for web search")
|
||||
|
||||
async def searchWeb(self, query: str, maxResults: int = 10) -> Dict[str, Any]:
|
||||
"""Search web content using Google search via SerpAPI"""
|
||||
try:
|
||||
if not self.srcApikey:
|
||||
return {
|
||||
"error": "SerpAPI key not configured",
|
||||
"query": query
|
||||
}
|
||||
|
||||
# Get user language from service container if available
|
||||
userLanguage = "en" # Default language
|
||||
if hasattr(self.serviceContainer, 'user') and hasattr(self.serviceContainer.user, 'language'):
|
||||
userLanguage = self.serviceContainer.user.language
|
||||
|
||||
# Format the search request for SerpAPI
|
||||
params = {
|
||||
"engine": self.srcEngine,
|
||||
"q": query,
|
||||
"api_key": self.srcApikey,
|
||||
"num": min(maxResults, self.maxResults), # Number of results to return
|
||||
"hl": userLanguage # User language
|
||||
}
|
||||
|
||||
# Make the API request
|
||||
response = requests.get("https://serpapi.com/search", params=params, timeout=self.timeout)
|
||||
response.raise_for_status()
|
||||
|
||||
# Parse JSON response
|
||||
search_results = response.json()
|
||||
|
||||
# Extract organic results
|
||||
results = []
|
||||
|
||||
if "organic_results" in search_results:
|
||||
for result in search_results["organic_results"][:maxResults]:
|
||||
# Extract title
|
||||
title = result.get("title", "No title")
|
||||
|
||||
# Extract URL
|
||||
url = result.get("link", "No URL")
|
||||
|
||||
# Extract snippet
|
||||
snippet = result.get("snippet", "No description")
|
||||
|
||||
# Get actual page content
|
||||
try:
|
||||
targetPageSoup = self._readUrl(url)
|
||||
content = self._extractMainContent(targetPageSoup)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error extracting content from {url}: {str(e)}")
|
||||
content = f"Error extracting content: {str(e)}"
|
||||
|
||||
results.append({
|
||||
'title': title,
|
||||
'url': url,
|
||||
'snippet': snippet,
|
||||
'content': content
|
||||
})
|
||||
|
||||
# Limit number of results
|
||||
if len(results) >= maxResults:
|
||||
break
|
||||
else:
|
||||
logger.warning(f"No organic results found in SerpAPI response for: {query}")
|
||||
|
||||
return {
|
||||
"query": query,
|
||||
"maxResults": maxResults,
|
||||
"results": results,
|
||||
"totalFound": len(results),
|
||||
"timestamp": datetime.now(UTC).isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching web: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"query": query
|
||||
}
|
||||
|
||||
async def crawlPage(self, url: str, depth: int = 1, followLinks: bool = True, extractContent: bool = True) -> Dict[str, Any]:
|
||||
"""Crawl web page and extract content"""
|
||||
try:
|
||||
# Read the URL
|
||||
soup = self._readUrl(url)
|
||||
if not soup:
|
||||
return {
|
||||
"error": "Failed to read URL",
|
||||
"url": url
|
||||
}
|
||||
|
||||
# Extract basic information
|
||||
title = self._extractTitle(soup, url)
|
||||
content = self._extractMainContent(soup) if extractContent else ""
|
||||
|
||||
# Extract links if requested
|
||||
links = []
|
||||
if followLinks:
|
||||
for link in soup.find_all('a', href=True):
|
||||
href = link.get('href')
|
||||
if href and href.startswith(('http://', 'https://')):
|
||||
links.append({
|
||||
'url': href,
|
||||
'text': link.get_text(strip=True)[:100]
|
||||
})
|
||||
|
||||
# Extract images
|
||||
images = []
|
||||
for img in soup.find_all('img', src=True):
|
||||
src = img.get('src')
|
||||
if src:
|
||||
images.append({
|
||||
'src': src,
|
||||
'alt': img.get('alt', ''),
|
||||
'title': img.get('title', '')
|
||||
})
|
||||
|
||||
return {
|
||||
"url": url,
|
||||
"depth": depth,
|
||||
"followLinks": followLinks,
|
||||
"extractContent": extractContent,
|
||||
"title": title,
|
||||
"content": content,
|
||||
"links": links[:10], # Limit to first 10 links
|
||||
"images": images[:10], # Limit to first 10 images
|
||||
"timestamp": datetime.now(UTC).isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error crawling web page: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"url": url
|
||||
}
|
||||
|
||||
async def extractContent(self, url: str, selectors: Dict[str, str] = None, format: str = "text") -> Dict[str, Any]:
|
||||
"""Extract content from web page using selectors"""
|
||||
try:
|
||||
# Read the URL
|
||||
soup = self._readUrl(url)
|
||||
if not soup:
|
||||
return {
|
||||
"error": "Failed to read URL",
|
||||
"url": url
|
||||
}
|
||||
|
||||
extracted_content = {}
|
||||
|
||||
if selectors:
|
||||
# Extract content using provided selectors
|
||||
for selector_name, selector in selectors.items():
|
||||
elements = soup.select(selector)
|
||||
if elements:
|
||||
if format == "text":
|
||||
extracted_content[selector_name] = [elem.get_text(strip=True) for elem in elements]
|
||||
elif format == "html":
|
||||
extracted_content[selector_name] = [str(elem) for elem in elements]
|
||||
else:
|
||||
extracted_content[selector_name] = [elem.get_text(strip=True) for elem in elements]
|
||||
else:
|
||||
extracted_content[selector_name] = []
|
||||
else:
|
||||
# Auto-extract common elements
|
||||
extracted_content = {
|
||||
"title": self._extractTitle(soup, url),
|
||||
"main_content": self._extractMainContent(soup),
|
||||
"headings": [h.get_text(strip=True) for h in soup.find_all(['h1', 'h2', 'h3'])],
|
||||
"links": [a.get('href') for a in soup.find_all('a', href=True) if a.get('href').startswith(('http://', 'https://'))],
|
||||
"images": [img.get('src') for img in soup.find_all('img', src=True)]
|
||||
}
|
||||
|
||||
return {
|
||||
"url": url,
|
||||
"selectors": selectors,
|
||||
"format": format,
|
||||
"content": extracted_content,
|
||||
"timestamp": datetime.now(UTC).isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting content: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"url": url
|
||||
}
|
||||
|
||||
async def validatePage(self, url: str, checks: List[str] = None) -> Dict[str, Any]:
|
||||
"""Validate web page for various criteria"""
|
||||
if checks is None:
|
||||
checks = ["accessibility", "seo", "performance"]
|
||||
|
||||
try:
|
||||
# Read the URL
|
||||
soup = self._readUrl(url)
|
||||
if not soup:
|
||||
return {
|
||||
"error": "Failed to read URL",
|
||||
"url": url
|
||||
}
|
||||
|
||||
validation_results = {}
|
||||
|
||||
for check in checks:
|
||||
if check == "accessibility":
|
||||
validation_results["accessibility"] = self._checkAccessibility(soup)
|
||||
elif check == "seo":
|
||||
validation_results["seo"] = self._checkSEO(soup)
|
||||
elif check == "performance":
|
||||
validation_results["performance"] = self._checkPerformance(soup, url)
|
||||
else:
|
||||
validation_results[check] = {"status": "unknown", "message": f"Unknown check type: {check}"}
|
||||
|
||||
return {
|
||||
"url": url,
|
||||
"checks": checks,
|
||||
"results": validation_results,
|
||||
"timestamp": datetime.now(UTC).isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating web page: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"url": url
|
||||
}
|
||||
|
||||
def _checkAccessibility(self, soup: BeautifulSoup) -> Dict[str, Any]:
|
||||
"""Check basic accessibility features"""
|
||||
issues = []
|
||||
warnings = []
|
||||
|
||||
# Check for alt text on images
|
||||
images_without_alt = soup.find_all('img', alt='')
|
||||
if images_without_alt:
|
||||
issues.append(f"Found {len(images_without_alt)} images without alt text")
|
||||
|
||||
# Check for proper heading structure
|
||||
headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
|
||||
if not headings:
|
||||
warnings.append("No headings found - poor document structure")
|
||||
|
||||
# Check for form labels
|
||||
forms = soup.find_all('form')
|
||||
for form in forms:
|
||||
inputs = form.find_all('input')
|
||||
for input_elem in inputs:
|
||||
if input_elem.get('type') not in ['submit', 'button', 'hidden']:
|
||||
if not input_elem.get('id') or not soup.find('label', attrs={'for': input_elem.get('id')}):
|
||||
warnings.append("Form input without proper label")
|
||||
|
||||
return {
|
||||
"status": "warning" if warnings else "pass",
|
||||
"issues": issues,
|
||||
"warnings": warnings
|
||||
}
|
||||
|
||||
def _checkSEO(self, soup: BeautifulSoup) -> Dict[str, Any]:
|
||||
"""Check basic SEO features"""
|
||||
issues = []
|
||||
warnings = []
|
||||
|
||||
# Check for title tag
|
||||
title = soup.find('title')
|
||||
if not title:
|
||||
issues.append("Missing title tag")
|
||||
elif len(title.get_text()) < 10:
|
||||
warnings.append("Title tag is too short")
|
||||
elif len(title.get_text()) > 60:
|
||||
warnings.append("Title tag is too long")
|
||||
|
||||
# Check for meta description
|
||||
meta_desc = soup.find('meta', attrs={'name': 'description'})
|
||||
if not meta_desc:
|
||||
warnings.append("Missing meta description")
|
||||
elif meta_desc.get('content'):
|
||||
if len(meta_desc.get('content')) < 50:
|
||||
warnings.append("Meta description is too short")
|
||||
elif len(meta_desc.get('content')) > 160:
|
||||
warnings.append("Meta description is too long")
|
||||
|
||||
# Check for h1 tag
|
||||
h1_tags = soup.find_all('h1')
|
||||
if not h1_tags:
|
||||
warnings.append("No H1 tag found")
|
||||
elif len(h1_tags) > 1:
|
||||
warnings.append("Multiple H1 tags found")
|
||||
|
||||
return {
|
||||
"status": "warning" if warnings else "pass",
|
||||
"issues": issues,
|
||||
"warnings": warnings
|
||||
}
|
||||
|
||||
def _checkPerformance(self, soup: BeautifulSoup, url: str) -> Dict[str, Any]:
|
||||
"""Check basic performance indicators"""
|
||||
warnings = []
|
||||
|
||||
# Count images
|
||||
images = soup.find_all('img')
|
||||
if len(images) > 20:
|
||||
warnings.append(f"Many images found ({len(images)}) - may impact loading speed")
|
||||
|
||||
# Check for external resources
|
||||
external_scripts = soup.find_all('script', src=True)
|
||||
external_styles = soup.find_all('link', rel='stylesheet')
|
||||
|
||||
if len(external_scripts) > 10:
|
||||
warnings.append(f"Many external scripts ({len(external_scripts)}) - may impact loading speed")
|
||||
|
||||
if len(external_styles) > 5:
|
||||
warnings.append(f"Many external stylesheets ({len(external_styles)}) - may impact loading speed")
|
||||
|
||||
return {
|
||||
"status": "warning" if warnings else "pass",
|
||||
"warnings": warnings,
|
||||
"metrics": {
|
||||
"images": len(images),
|
||||
"external_scripts": len(external_scripts),
|
||||
"external_styles": len(external_styles)
|
||||
}
|
||||
}
|
||||
|
||||
def _readUrl(self, url: str) -> BeautifulSoup:
|
||||
"""Read a URL and return a BeautifulSoup parser for the content"""
|
||||
if not url or not url.startswith(('http://', 'https://')):
|
||||
return None
|
||||
|
||||
headers = {
|
||||
'User-Agent': self.user_agent,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
}
|
||||
|
||||
try:
|
||||
# Initial request
|
||||
response = requests.get(url, headers=headers, timeout=self.timeout)
|
||||
|
||||
# Handling for status 202
|
||||
if response.status_code == 202:
|
||||
# Retry with backoff
|
||||
backoff_times = [0.5, 1.0, 2.0, 5.0]
|
||||
|
||||
for wait_time in backoff_times:
|
||||
time.sleep(wait_time)
|
||||
response = requests.get(url, headers=headers, timeout=self.timeout)
|
||||
|
||||
if response.status_code != 202:
|
||||
break
|
||||
|
||||
# Raise for error status codes
|
||||
response.raise_for_status()
|
||||
|
||||
# Parse HTML
|
||||
return BeautifulSoup(response.text, 'html.parser')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading URL {url}: {str(e)}")
|
||||
return None
|
||||
|
||||
def _extractTitle(self, soup: BeautifulSoup, url: str) -> str:
|
||||
"""Extract the title from a webpage"""
|
||||
if not soup:
|
||||
return f"Error with {url}"
|
||||
|
||||
# Extract title from title tag
|
||||
title_tag = soup.find('title')
|
||||
title = title_tag.text.strip() if title_tag else "No title"
|
||||
|
||||
# Alternative: Also look for h1 tags if title tag is missing
|
||||
if title == "No title":
|
||||
h1_tag = soup.find('h1')
|
||||
if h1_tag:
|
||||
title = h1_tag.text.strip()
|
||||
|
||||
return title
|
||||
|
||||
def _extractMainContent(self, soup: BeautifulSoup, max_chars: int = 10000) -> str:
|
||||
"""Extract the main content from an HTML page"""
|
||||
if not soup:
|
||||
return ""
|
||||
|
||||
# Try to find main content elements in priority order
|
||||
main_content = None
|
||||
for selector in ['main', 'article', '#content', '.content', '#main', '.main']:
|
||||
content = soup.select_one(selector)
|
||||
if content:
|
||||
main_content = content
|
||||
break
|
||||
|
||||
# If no main content found, use the body
|
||||
if not main_content:
|
||||
main_content = soup.find('body') or soup
|
||||
|
||||
# Remove script, style, nav, footer elements that don't contribute to main content
|
||||
for element in main_content.select('script, style, nav, footer, header, aside, .sidebar, #sidebar, .comments, #comments, .advertisement, .ads, iframe'):
|
||||
element.extract()
|
||||
|
||||
# Extract text content
|
||||
text_content = main_content.get_text(separator=' ', strip=True)
|
||||
|
||||
# Limit to max_chars
|
||||
return text_content[:max_chars]
|
||||
|
||||
class MethodWeb(MethodBase):
|
||||
"""Web method implementation for web operations"""
|
||||
|
||||
def __init__(self, serviceContainer: Any):
|
||||
"""Initialize the web method"""
|
||||
super().__init__(serviceContainer)
|
||||
self.name = "web"
|
||||
self.description = "Handle web operations like searching and crawling"
|
||||
self.webService = WebService(serviceContainer)
|
||||
|
||||
@action
|
||||
async def search(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Search web content
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
query: Search query
|
||||
engine: Search engine to use (google, bing)
|
||||
maxResults: Maximum number of results
|
||||
"""
|
||||
async def search(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Search web content"""
|
||||
try:
|
||||
query = parameters["query"]
|
||||
engine = parameters.get("engine", "google")
|
||||
query = parameters.get("query")
|
||||
maxResults = parameters.get("maxResults", 10)
|
||||
|
||||
if not query:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="Search query is required"
|
||||
)
|
||||
|
||||
# Search web
|
||||
results = await self.webService.searchContent(
|
||||
results = await self.webService.searchWeb(
|
||||
query=query,
|
||||
engine=engine,
|
||||
maxResults=maxResults
|
||||
)
|
||||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"query": query,
|
||||
"engine": engine,
|
||||
"results": results
|
||||
}
|
||||
data=results
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching web: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def crawl(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Crawl web page
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
url: URL to crawl
|
||||
depth: Crawl depth
|
||||
followLinks: Whether to follow links
|
||||
extractContent: Whether to extract content
|
||||
"""
|
||||
async def crawl(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Crawl web page"""
|
||||
try:
|
||||
url = parameters["url"]
|
||||
url = parameters.get("url")
|
||||
depth = parameters.get("depth", 1)
|
||||
followLinks = parameters.get("followLinks", False)
|
||||
followLinks = parameters.get("followLinks", True)
|
||||
extractContent = parameters.get("extractContent", True)
|
||||
|
||||
if not url:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="URL is required"
|
||||
)
|
||||
|
||||
# Crawl page
|
||||
results = await self.webService.crawlPage(
|
||||
url=url,
|
||||
|
|
@ -87,36 +505,32 @@ class MethodWeb(MethodBase):
|
|||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"url": url,
|
||||
"depth": depth,
|
||||
"results": results
|
||||
}
|
||||
data=results
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error crawling web page: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def extract(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Extract content from web page
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
url: URL to extract from
|
||||
selectors: CSS selectors to extract
|
||||
format: Output format (text, html, json)
|
||||
"""
|
||||
async def extract(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Extract content from web page"""
|
||||
try:
|
||||
url = parameters["url"]
|
||||
selectors = parameters.get("selectors", ["body"])
|
||||
url = parameters.get("url")
|
||||
selectors = parameters.get("selectors", {})
|
||||
format = parameters.get("format", "text")
|
||||
|
||||
if not url:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="URL is required"
|
||||
)
|
||||
|
||||
# Extract content
|
||||
content = await self.webService.extractContent(
|
||||
url=url,
|
||||
|
|
@ -126,34 +540,31 @@ class MethodWeb(MethodBase):
|
|||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"url": url,
|
||||
"format": format,
|
||||
"content": content
|
||||
}
|
||||
data=content
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting web content: {str(e)}")
|
||||
logger.error(f"Error extracting content: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@action
|
||||
async def validate(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
"""
|
||||
Validate web page
|
||||
|
||||
Args:
|
||||
parameters:
|
||||
url: URL to validate
|
||||
checks: List of checks to perform
|
||||
"""
|
||||
async def validate(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
"""Validate web page"""
|
||||
try:
|
||||
url = parameters["url"]
|
||||
url = parameters.get("url")
|
||||
checks = parameters.get("checks", ["accessibility", "seo", "performance"])
|
||||
|
||||
if not url:
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="URL is required"
|
||||
)
|
||||
|
||||
# Validate page
|
||||
results = await self.webService.validatePage(
|
||||
url=url,
|
||||
|
|
@ -162,16 +573,13 @@ class MethodWeb(MethodBase):
|
|||
|
||||
return self._createResult(
|
||||
success=True,
|
||||
data={
|
||||
"url": url,
|
||||
"checks": checks,
|
||||
"results": results
|
||||
}
|
||||
data=results
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating web page: {str(e)}")
|
||||
return self._createResult(
|
||||
success=False,
|
||||
data={"error": str(e)}
|
||||
data={},
|
||||
error=str(e)
|
||||
)
|
||||
|
|
@ -20,7 +20,7 @@ import xml.etree.ElementTree as ET
|
|||
import os
|
||||
import random
|
||||
from io import StringIO
|
||||
from patterns import Pattern, HeaderPatterns, DataPatterns, get_pattern_for_header, find_patterns_in_text, TextTablePatterns
|
||||
from modules.neutralizer.patterns import Pattern, HeaderPatterns, DataPatterns, get_pattern_for_header, find_patterns_in_text, TextTablePatterns
|
||||
import base64
|
||||
|
||||
# Configure logging
|
||||
|
|
|
|||
|
|
@ -10,14 +10,16 @@ from modules.interfaces.interfaceChatModel import (
|
|||
TaskStatus, ChatDocument, TaskItem, TaskAction, TaskResult, ChatStat, ChatLog, ChatMessage, ChatWorkflow
|
||||
)
|
||||
from modules.workflow.serviceContainer import ServiceContainer
|
||||
from modules.interfaces.interfaceChatObjects import ChatObjects
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ChatManager:
|
||||
"""Chat manager with improved AI integration and method handling"""
|
||||
|
||||
def __init__(self, currentUser: User):
|
||||
def __init__(self, currentUser: User, chatInterface: ChatObjects):
|
||||
self.currentUser = currentUser
|
||||
self.chatInterface = chatInterface
|
||||
self.service: ServiceContainer = None
|
||||
self.workflow: ChatWorkflow = None
|
||||
|
||||
|
|
@ -59,15 +61,14 @@ class ChatManager:
|
|||
logger.error("Actions must be a list")
|
||||
return None
|
||||
|
||||
# Create task
|
||||
task = TaskItem(
|
||||
id=str(uuid.uuid4()),
|
||||
workflow=workflow,
|
||||
userInput=initialMessage.message,
|
||||
status=taskDef["status"],
|
||||
feedback=taskDef["feedback"],
|
||||
actions=[]
|
||||
)
|
||||
# Create task using interface
|
||||
taskData = {
|
||||
"workflowId": workflow.id,
|
||||
"userInput": initialMessage.message,
|
||||
"status": taskDef["status"],
|
||||
"feedback": taskDef["feedback"],
|
||||
"actionList": []
|
||||
}
|
||||
|
||||
# Add actions
|
||||
for actionDef in taskDef["actions"]:
|
||||
|
|
@ -80,13 +81,15 @@ class ChatManager:
|
|||
|
||||
action = TaskAction(
|
||||
id=str(uuid.uuid4()),
|
||||
method=actionDef["method"],
|
||||
action=actionDef["action"],
|
||||
parameters=actionDef["parameters"],
|
||||
resultLabel=actionDef.get("resultLabel")
|
||||
execMethod=actionDef["method"],
|
||||
execAction=actionDef["action"],
|
||||
execParameters=actionDef["parameters"],
|
||||
execResultLabel=actionDef.get("resultLabel")
|
||||
)
|
||||
task.actions.append(action)
|
||||
taskData["actionList"].append(action)
|
||||
|
||||
# Create task using interface
|
||||
task = self.chatInterface.createTask(taskData)
|
||||
return task
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -129,15 +132,14 @@ class ChatManager:
|
|||
logger.error("Actions must be a list")
|
||||
return None
|
||||
|
||||
# Create task
|
||||
task = TaskItem(
|
||||
id=str(uuid.uuid4()),
|
||||
workflow=workflow,
|
||||
userInput=previousResult.feedback,
|
||||
status=taskDef["status"],
|
||||
feedback=taskDef["feedback"],
|
||||
actions=[]
|
||||
)
|
||||
# Create task using interface
|
||||
taskData = {
|
||||
"workflowId": workflow.id,
|
||||
"userInput": previousResult.feedback,
|
||||
"status": taskDef["status"],
|
||||
"feedback": taskDef["feedback"],
|
||||
"actionList": []
|
||||
}
|
||||
|
||||
# Add actions
|
||||
for actionDef in taskDef["actions"]:
|
||||
|
|
@ -150,13 +152,15 @@ class ChatManager:
|
|||
|
||||
action = TaskAction(
|
||||
id=str(uuid.uuid4()),
|
||||
method=actionDef["method"],
|
||||
action=actionDef["action"],
|
||||
parameters=actionDef["parameters"],
|
||||
resultLabel=actionDef.get("resultLabel")
|
||||
execMethod=actionDef["method"],
|
||||
execAction=actionDef["action"],
|
||||
execParameters=actionDef["parameters"],
|
||||
execResultLabel=actionDef.get("resultLabel")
|
||||
)
|
||||
task.actions.append(action)
|
||||
taskData["actionList"].append(action)
|
||||
|
||||
# Create task using interface
|
||||
task = self.chatInterface.createTask(taskData)
|
||||
return task
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -167,12 +171,12 @@ class ChatManager:
|
|||
"""Execute a task's actions"""
|
||||
try:
|
||||
# Execute each action
|
||||
for action in task.actions:
|
||||
for action in task.actionList:
|
||||
# Create action prompt
|
||||
prompt = f"""Execute the following action:
|
||||
|
||||
Action: {action.method}.{action.action}
|
||||
Parameters: {json.dumps(action.parameters)}
|
||||
Action: {action.execMethod}.{action.execAction}
|
||||
Parameters: {json.dumps(action.execParameters)}
|
||||
|
||||
Please provide a JSON response with:
|
||||
1. result: The result of the action
|
||||
|
|
@ -206,26 +210,31 @@ Example format:
|
|||
action.status = "completed" if not result.get("error") else "failed"
|
||||
action.result = result.get("result", "")
|
||||
action.error = result.get("error", "")
|
||||
action.resultLabel = result.get("resultLabel", "")
|
||||
action.execResultLabel = result.get("resultLabel", "")
|
||||
|
||||
# Create message for action result
|
||||
message = ChatMessage(
|
||||
id=str(uuid.uuid4()),
|
||||
workflow=task.workflow,
|
||||
role="assistant",
|
||||
content=action.result,
|
||||
status="step",
|
||||
actionId=action.id,
|
||||
documentsLabel=action.resultLabel
|
||||
)
|
||||
task.workflow.messages.append(message)
|
||||
# Create message for action result using interface
|
||||
messageData = {
|
||||
"workflowId": task.workflowId,
|
||||
"role": "assistant",
|
||||
"message": action.result,
|
||||
"status": "step",
|
||||
"sequenceNr": len(self.workflow.messages) + 1,
|
||||
"publishedAt": datetime.now(UTC).isoformat(),
|
||||
"actionId": action.id,
|
||||
"actionMethod": action.execMethod,
|
||||
"actionName": action.execAction,
|
||||
"documentsLabel": action.execResultLabel
|
||||
}
|
||||
message = self.chatInterface.createWorkflowMessage(messageData)
|
||||
if message:
|
||||
self.workflow.messages.append(message)
|
||||
|
||||
# If action failed, stop execution
|
||||
if action.status == "failed":
|
||||
break
|
||||
|
||||
# Update task status
|
||||
task.status = "completed" if all(a.status == "completed" for a in task.actions) else "failed"
|
||||
task.status = "completed" if all(a.status == "completed" for a in task.actionList) else "failed"
|
||||
|
||||
return task
|
||||
|
||||
|
|
@ -237,24 +246,25 @@ Example format:
|
|||
async def parseTaskResult(self, workflow: ChatWorkflow, task: TaskItem) -> None:
|
||||
"""Parse and process task results"""
|
||||
try:
|
||||
# Create result message
|
||||
message = ChatMessage(
|
||||
id=str(uuid.uuid4()),
|
||||
workflow=workflow,
|
||||
role="assistant",
|
||||
content=task.feedback,
|
||||
status="step",
|
||||
actionId=task.id,
|
||||
documentsLabel=task.resultLabel
|
||||
)
|
||||
workflow.messages.append(message)
|
||||
# Create result message using interface
|
||||
messageData = {
|
||||
"workflowId": workflow.id,
|
||||
"role": "assistant",
|
||||
"message": task.feedback,
|
||||
"status": "step",
|
||||
"sequenceNr": len(workflow.messages) + 1,
|
||||
"publishedAt": datetime.now(UTC).isoformat(),
|
||||
"actionId": task.id
|
||||
}
|
||||
message = self.chatInterface.createWorkflowMessage(messageData)
|
||||
if message:
|
||||
workflow.messages.append(message)
|
||||
|
||||
# Update workflow stats
|
||||
if task.processingTime:
|
||||
if not workflow.stats:
|
||||
workflow.stats = ChatStat()
|
||||
workflow.stats.processingTime = (workflow.stats.processingTime or 0) + task.processingTime
|
||||
self.service.updateWorkflow(workflow.id, {"stats": workflow.stats.dict()})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing task result: {str(e)}")
|
||||
|
|
@ -470,4 +480,30 @@ Example Format:
|
|||
]
|
||||
}}
|
||||
|
||||
Please provide the task definition in JSON format following these rules."""
|
||||
Please provide the task definition in JSON format following these rules."""
|
||||
|
||||
# ===== Utility Methods =====
|
||||
async def processFileIds(self, fileIds: List[str]) -> List[ChatDocument]:
|
||||
"""Process file IDs and return ChatDocument objects"""
|
||||
documents = []
|
||||
for fileId in fileIds:
|
||||
try:
|
||||
# Get file info from service
|
||||
fileInfo = self.service.getFileInfo(fileId)
|
||||
if fileInfo:
|
||||
document = ChatDocument(
|
||||
id=str(uuid.uuid4()),
|
||||
fileId=fileId,
|
||||
filename=fileInfo.get("filename", "unknown"),
|
||||
fileSize=fileInfo.get("size", 0),
|
||||
mimeType=fileInfo.get("mimeType", "application/octet-stream")
|
||||
)
|
||||
documents.append(document)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing file ID {fileId}: {str(e)}")
|
||||
return documents
|
||||
|
||||
def setUserLanguage(self, language: str) -> None:
|
||||
"""Set user language for the chat manager"""
|
||||
if hasattr(self, 'service') and self.service:
|
||||
self.service.user.language = language
|
||||
|
|
@ -2,15 +2,12 @@
|
|||
Document Manager Module for handling document operations and content extraction.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from modules.interfaces.interfaceChatModel import (
|
||||
ChatDocument,
|
||||
ExtractedContent
|
||||
)
|
||||
from modules.workflow.serviceContainer import ServiceContainer
|
||||
from modules.workflow.processorDocument import DocumentProcessor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -18,14 +15,58 @@ logger = logging.getLogger(__name__)
|
|||
class DocumentManager:
|
||||
"""Manager for document operations and content extraction"""
|
||||
|
||||
def __init__(self, serviceContainer: ServiceContainer):
|
||||
def __init__(self, serviceContainer):
|
||||
self.service = serviceContainer
|
||||
self._processor = DocumentProcessor(serviceContainer)
|
||||
# Create processor without any dependencies
|
||||
self._processor = DocumentProcessor()
|
||||
|
||||
async def extractContent(self, prompt: str, document: ChatDocument) -> ExtractedContent:
|
||||
"""Extract content from document using prompt"""
|
||||
async def extractContentFromDocument(self, prompt: str, document: ChatDocument) -> ExtractedContent:
|
||||
"""Extract content from ChatDocument using prompt"""
|
||||
try:
|
||||
return await self._processor.processDocument(document, prompt)
|
||||
# Extract file data from ChatDocument
|
||||
if document.data:
|
||||
fileData = document.data.encode('utf-8') if isinstance(document.data, str) else document.data
|
||||
else:
|
||||
# Try to get file data from service container if document has fileId
|
||||
if hasattr(document, 'fileId') and document.fileId:
|
||||
fileData = self.service.getFileData(document.fileId)
|
||||
else:
|
||||
logger.error(f"No file data available in document: {document}")
|
||||
raise ValueError("No file data available in document")
|
||||
|
||||
# Get filename and mime type from document
|
||||
filename = document.filename if hasattr(document, 'filename') else "document"
|
||||
mimeType = document.mimeType if hasattr(document, 'mimeType') else "application/octet-stream"
|
||||
|
||||
# Process with processor
|
||||
extractedContent = await self._processor.processFileData(
|
||||
fileData=fileData,
|
||||
filename=filename,
|
||||
mimeType=mimeType,
|
||||
base64Encoded=False,
|
||||
prompt=prompt
|
||||
)
|
||||
|
||||
# Update objectId to match document ID
|
||||
extractedContent.objectId = document.id
|
||||
extractedContent.objectType = "ChatDocument"
|
||||
|
||||
return extractedContent
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting from document: {str(e)}")
|
||||
raise
|
||||
|
||||
async def extractContentFromFileData(self, prompt: str, fileData: bytes, filename: str, mimeType: str, base64Encoded: bool = False) -> ExtractedContent:
|
||||
"""Extract content from file data directly using prompt"""
|
||||
try:
|
||||
return await self._processor.processFileData(
|
||||
fileData=fileData,
|
||||
filename=filename,
|
||||
mimeType=mimeType,
|
||||
base64Encoded=base64Encoded,
|
||||
prompt=prompt
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting from file data: {str(e)}")
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import uuid
|
|||
|
||||
from modules.interfaces.interfaceAppObjects import User
|
||||
|
||||
from modules.interfaces.interfaceChatModel import (UserInputRequest, ChatMessage, ChatWorkflow)
|
||||
from modules.interfaces.interfaceChatModel import (UserInputRequest, ChatMessage, ChatWorkflow, TaskItem)
|
||||
from modules.interfaces.interfaceChatObjects import ChatObjects
|
||||
from modules.workflow.managerChat import ChatManager
|
||||
|
||||
|
|
@ -20,15 +20,40 @@ class WorkflowManager:
|
|||
|
||||
def __init__(self, chatInterface: ChatObjects, currentUser: User):
|
||||
self.chatInterface = chatInterface
|
||||
self.chatManager = ChatManager(currentUser)
|
||||
self.chatManager = ChatManager(currentUser, chatInterface)
|
||||
self.currentUser = currentUser
|
||||
|
||||
def _checkWorkflowStopped(self, workflow: ChatWorkflow) -> None:
|
||||
"""Check if workflow has been stopped"""
|
||||
if workflow.status == "stopped":
|
||||
raise WorkflowStoppedException("Workflow was stopped by user")
|
||||
|
||||
async def workflowProcess(self, userInput: UserInputRequest, workflow: ChatWorkflow) -> None:
|
||||
|
||||
async def workflowProcess(self, userInput: UserInputRequest, workflow: ChatWorkflow) -> TaskItem:
|
||||
"""Process a workflow with user input"""
|
||||
|
||||
# Initialize chat manager
|
||||
await self.chatManager.initialize(workflow)
|
||||
|
||||
# Set user language
|
||||
self.chatManager.setUserLanguage(userInput.userLanguage)
|
||||
|
||||
# Send first message
|
||||
message = await self._sendFirstMessage(userInput, workflow)
|
||||
|
||||
# Create initial task
|
||||
task = await self.chatManager.createInitialTask(workflow, message)
|
||||
|
||||
# Log the task object
|
||||
logger.info(f"Created task: {task}")
|
||||
if task:
|
||||
logger.info(f"Task ID: {task.id}")
|
||||
logger.info(f"Task Status: {task.status}")
|
||||
logger.info(f"Task Feedback: {task.feedback}")
|
||||
logger.info(f"Number of actions: {len(task.actionList) if task.actionList else 0}")
|
||||
|
||||
return task
|
||||
|
||||
async def workflowProcess_ORIGINAL_TEMPORARY_DEACTIVATED(self, userInput: UserInputRequest, workflow: ChatWorkflow) -> None:
|
||||
"""Process a workflow with user input"""
|
||||
try:
|
||||
# Initialize chat manager
|
||||
|
|
@ -78,24 +103,29 @@ class WorkflowManager:
|
|||
async def _sendFirstMessage(self, userInput: UserInputRequest, workflow: ChatWorkflow) -> ChatMessage:
|
||||
"""Send first message to start workflow"""
|
||||
try:
|
||||
# Create initial message
|
||||
message = ChatMessage(
|
||||
id=str(uuid.uuid4()),
|
||||
workflowId=workflow.id,
|
||||
role="user",
|
||||
message=userInput.prompt,
|
||||
status="first",
|
||||
sequenceNr=1,
|
||||
publishedAt=datetime.now(UTC).isoformat()
|
||||
)
|
||||
# Create initial message using interface
|
||||
messageData = {
|
||||
"workflowId": workflow.id,
|
||||
"role": "user",
|
||||
"message": userInput.prompt,
|
||||
"status": "first",
|
||||
"sequenceNr": 1,
|
||||
"publishedAt": datetime.now(UTC).isoformat()
|
||||
}
|
||||
|
||||
# Add documents if any
|
||||
if userInput.listFileId:
|
||||
message.documents = await self.chatManager.processFileIds(userInput.listFileId)
|
||||
# Process file IDs and add to message data
|
||||
documents = await self.chatManager.processFileIds(userInput.listFileId)
|
||||
messageData["documents"] = documents
|
||||
|
||||
# Add message to workflow
|
||||
workflow.messages.append(message)
|
||||
return message
|
||||
# Create message using interface
|
||||
message = self.chatInterface.createWorkflowMessage(messageData)
|
||||
if message:
|
||||
workflow.messages.append(message)
|
||||
return message
|
||||
else:
|
||||
raise Exception("Failed to create first message")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending first message: {str(e)}")
|
||||
|
|
@ -107,19 +137,20 @@ class WorkflowManager:
|
|||
# Generate feedback
|
||||
feedback = await self.chatManager.generateWorkflowFeedback(workflow)
|
||||
|
||||
# Create last message
|
||||
message = ChatMessage(
|
||||
id=str(uuid.uuid4()),
|
||||
workflowId=workflow.id,
|
||||
role="assistant",
|
||||
message=feedback,
|
||||
status="last",
|
||||
sequenceNr=len(workflow.messages) + 1,
|
||||
publishedAt=datetime.now(UTC).isoformat()
|
||||
)
|
||||
# Create last message using interface
|
||||
messageData = {
|
||||
"workflowId": workflow.id,
|
||||
"role": "assistant",
|
||||
"message": feedback,
|
||||
"status": "last",
|
||||
"sequenceNr": len(workflow.messages) + 1,
|
||||
"publishedAt": datetime.now(UTC).isoformat()
|
||||
}
|
||||
|
||||
# Add message to workflow
|
||||
workflow.messages.append(message)
|
||||
# Create message using interface
|
||||
message = self.chatInterface.createWorkflowMessage(messageData)
|
||||
if message:
|
||||
workflow.messages.append(message)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending last message: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -8,16 +8,15 @@ from datetime import datetime, UTC
|
|||
from pathlib import Path
|
||||
import xml.etree.ElementTree as ET
|
||||
from bs4 import BeautifulSoup
|
||||
import uuid
|
||||
|
||||
from modules.interfaces.interfaceChatModel import (
|
||||
ChatDocument,
|
||||
ExtractedContent,
|
||||
ContentItem,
|
||||
ContentMetadata
|
||||
)
|
||||
from modules.neutralizer.neutralizer import DataAnonymizer
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.workflow.serviceContainer import ServiceContainer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -33,12 +32,11 @@ class FileProcessingError(Exception):
|
|||
class DocumentProcessor:
|
||||
"""Processor for handling document operations and content extraction."""
|
||||
|
||||
def __init__(self, serviceContainer: ServiceContainer):
|
||||
def __init__(self):
|
||||
"""Initialize the document processor."""
|
||||
self.service = serviceContainer
|
||||
self._neutralizer = DataAnonymizer() if APP_CONFIG.get("ENABLE_CONTENT_NEUTRALIZATION", False) else None
|
||||
|
||||
self.supportedTypes: Dict[str, Callable[[ChatDocument], Awaitable[List[ContentItem]]]] = {
|
||||
self.supportedTypes: Dict[str, Callable[[bytes, str, str], Awaitable[List[ContentItem]]]] = {
|
||||
'text/plain': self._processText,
|
||||
'text/csv': self._processCsv,
|
||||
'application/json': self._processJson,
|
||||
|
|
@ -111,23 +109,15 @@ class DocumentProcessor:
|
|||
except ImportError as e:
|
||||
logger.warning(f"Image processing libraries could not be loaded: {e}")
|
||||
|
||||
async def _getFileData(self, document: ChatDocument) -> bytes:
|
||||
"""Centralized function to get file data"""
|
||||
try:
|
||||
fileData = self.service.getFileData(document.fileId)
|
||||
if fileData is None:
|
||||
raise FileProcessingError(f"Could not get file data for {document.fileId}")
|
||||
return fileData
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting file data: {str(e)}")
|
||||
raise FileProcessingError(f"Failed to get file data: {str(e)}")
|
||||
|
||||
async def processDocument(self, document: ChatDocument, prompt: str) -> ExtractedContent:
|
||||
async def processFileData(self, fileData: bytes, filename: str, mimeType: str, base64Encoded: bool = False, prompt: str = None) -> ExtractedContent:
|
||||
"""
|
||||
Process a document and extract its contents with AI processing.
|
||||
Process file data directly and extract its contents with AI processing.
|
||||
|
||||
Args:
|
||||
document: The document to process
|
||||
fileData: Raw file data as bytes
|
||||
filename: Name of the file
|
||||
mimeType: MIME type of the file
|
||||
base64Encoded: Whether the data is base64 encoded
|
||||
prompt: Prompt for AI content extraction
|
||||
|
||||
Returns:
|
||||
|
|
@ -137,19 +127,22 @@ class DocumentProcessor:
|
|||
FileProcessingError: If document processing fails
|
||||
"""
|
||||
try:
|
||||
# Get content type
|
||||
contentType = document.mimeType
|
||||
if contentType == "application/octet-stream":
|
||||
# Try to detect actual file type
|
||||
contentType = self._detectContentType(document)
|
||||
# Decode base64 if needed
|
||||
if base64Encoded:
|
||||
fileData = base64.b64decode(fileData)
|
||||
|
||||
if contentType not in self.supportedTypes:
|
||||
# Detect content type if needed
|
||||
if mimeType == "application/octet-stream":
|
||||
mimeType = self._detectContentTypeFromData(fileData, filename)
|
||||
|
||||
# Process document based on type
|
||||
if mimeType not in self.supportedTypes:
|
||||
# Fallback to binary processing
|
||||
contentItems = await self._processBinary(document)
|
||||
contentItems = await self._processBinary(fileData, filename, mimeType)
|
||||
else:
|
||||
# Process document based on type
|
||||
processor = self.supportedTypes[contentType]
|
||||
contentItems = await processor(document)
|
||||
processor = self.supportedTypes[mimeType]
|
||||
contentItems = await processor(fileData, filename, mimeType)
|
||||
|
||||
# Process with AI if prompt provided
|
||||
if prompt and contentItems:
|
||||
|
|
@ -161,20 +154,20 @@ class DocumentProcessor:
|
|||
logger.error(f"Error processing content with AI: {str(e)}")
|
||||
|
||||
return ExtractedContent(
|
||||
objectId=document.id,
|
||||
objectType="ChatDocument",
|
||||
objectId=str(uuid.uuid4()),
|
||||
objectType="FileData",
|
||||
contents=contentItems
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing document: {str(e)}")
|
||||
raise FileProcessingError(f"Failed to process document: {str(e)}")
|
||||
logger.error(f"Error processing file data: {str(e)}")
|
||||
raise FileProcessingError(f"Failed to process file data: {str(e)}")
|
||||
|
||||
def _detectContentType(self, document: ChatDocument) -> str:
|
||||
"""Detect content type from file content"""
|
||||
def _detectContentTypeFromData(self, fileData: bytes, filename: str) -> str:
|
||||
"""Detect content type from file data and filename"""
|
||||
try:
|
||||
# Check file extension first
|
||||
ext = os.path.splitext(document.filename)[1].lower()
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if ext:
|
||||
# Map common extensions to MIME types
|
||||
extToMime = {
|
||||
|
|
@ -200,16 +193,35 @@ class DocumentProcessor:
|
|||
if ext in extToMime:
|
||||
return extToMime[ext]
|
||||
|
||||
# Try to detect from content
|
||||
if fileData.startswith(b'%PDF'):
|
||||
return 'application/pdf'
|
||||
elif fileData.startswith(b'PK\x03\x04'):
|
||||
# ZIP-based formats (docx, xlsx, pptx)
|
||||
return 'application/zip'
|
||||
elif fileData.startswith(b'<'):
|
||||
# XML-based formats
|
||||
try:
|
||||
text = fileData.decode('utf-8', errors='ignore')
|
||||
if '<svg' in text.lower():
|
||||
return 'image/svg+xml'
|
||||
elif '<html' in text.lower():
|
||||
return 'text/html'
|
||||
else:
|
||||
return 'application/xml'
|
||||
except:
|
||||
pass
|
||||
|
||||
return 'application/octet-stream'
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error detecting content type: {str(e)}")
|
||||
logger.error(f"Error detecting content type from data: {str(e)}")
|
||||
return 'application/octet-stream'
|
||||
|
||||
async def _processText(self, document: ChatDocument) -> List[ContentItem]:
|
||||
async def _processText(self, fileData: bytes, filename: str, mimeType: str) -> List[ContentItem]:
|
||||
"""Process text document"""
|
||||
try:
|
||||
content = (await self._getFileData(document)).decode('utf-8')
|
||||
content = fileData.decode('utf-8')
|
||||
return [ContentItem(
|
||||
label="main",
|
||||
data=content,
|
||||
|
|
@ -224,10 +236,10 @@ class DocumentProcessor:
|
|||
logger.error(f"Error processing text document: {str(e)}")
|
||||
raise FileProcessingError(f"Failed to process text document: {str(e)}")
|
||||
|
||||
async def _processCsv(self, document: ChatDocument) -> List[ContentItem]:
|
||||
async def _processCsv(self, fileData: bytes, filename: str, mimeType: str) -> List[ContentItem]:
|
||||
"""Process CSV document"""
|
||||
try:
|
||||
content = (await self._getFileData(document)).decode('utf-8')
|
||||
content = fileData.decode('utf-8')
|
||||
return [ContentItem(
|
||||
label="main",
|
||||
data=content,
|
||||
|
|
@ -242,10 +254,10 @@ class DocumentProcessor:
|
|||
logger.error(f"Error processing CSV document: {str(e)}")
|
||||
raise FileProcessingError(f"Failed to process CSV document: {str(e)}")
|
||||
|
||||
async def _processJson(self, document: ChatDocument) -> List[ContentItem]:
|
||||
async def _processJson(self, fileData: bytes, filename: str, mimeType: str) -> List[ContentItem]:
|
||||
"""Process JSON document"""
|
||||
try:
|
||||
content = (await self._getFileData(document)).decode('utf-8')
|
||||
content = fileData.decode('utf-8')
|
||||
# Parse JSON to validate
|
||||
jsonData = json.loads(content)
|
||||
|
||||
|
|
@ -263,10 +275,10 @@ class DocumentProcessor:
|
|||
logger.error(f"Error processing JSON document: {str(e)}")
|
||||
raise FileProcessingError(f"Failed to process JSON document: {str(e)}")
|
||||
|
||||
async def _processXml(self, document: ChatDocument) -> List[ContentItem]:
|
||||
async def _processXml(self, fileData: bytes, filename: str, mimeType: str) -> List[ContentItem]:
|
||||
"""Process XML document"""
|
||||
try:
|
||||
content = (await self._getFileData(document)).decode('utf-8')
|
||||
content = fileData.decode('utf-8')
|
||||
return [ContentItem(
|
||||
label="main",
|
||||
data=content,
|
||||
|
|
@ -281,10 +293,10 @@ class DocumentProcessor:
|
|||
logger.error(f"Error processing XML document: {str(e)}")
|
||||
raise FileProcessingError(f"Failed to process XML document: {str(e)}")
|
||||
|
||||
async def _processHtml(self, document: ChatDocument) -> List[ContentItem]:
|
||||
async def _processHtml(self, fileData: bytes, filename: str, mimeType: str) -> List[ContentItem]:
|
||||
"""Process HTML document"""
|
||||
try:
|
||||
content = (await self._getFileData(document)).decode('utf-8')
|
||||
content = fileData.decode('utf-8')
|
||||
return [ContentItem(
|
||||
label="main",
|
||||
data=content,
|
||||
|
|
@ -299,10 +311,10 @@ class DocumentProcessor:
|
|||
logger.error(f"Error processing HTML document: {str(e)}")
|
||||
raise FileProcessingError(f"Failed to process HTML document: {str(e)}")
|
||||
|
||||
async def _processSvg(self, document: ChatDocument) -> List[ContentItem]:
|
||||
async def _processSvg(self, fileData: bytes, filename: str, mimeType: str) -> List[ContentItem]:
|
||||
"""Process SVG document"""
|
||||
try:
|
||||
content = (await self._getFileData(document)).decode('utf-8')
|
||||
content = fileData.decode('utf-8')
|
||||
# Check if it's actually SVG
|
||||
isSvg = "<svg" in content.lower()
|
||||
|
||||
|
|
@ -320,15 +332,13 @@ class DocumentProcessor:
|
|||
logger.error(f"Error processing SVG document: {str(e)}")
|
||||
raise FileProcessingError(f"Failed to process SVG document: {str(e)}")
|
||||
|
||||
async def _processImage(self, document: ChatDocument) -> List[ContentItem]:
|
||||
async def _processImage(self, fileData: bytes, filename: str, mimeType: str) -> List[ContentItem]:
|
||||
"""Process image document"""
|
||||
try:
|
||||
self._loadImageProcessor()
|
||||
if not imageProcessorLoaded:
|
||||
raise FileProcessingError("Image processing libraries not available")
|
||||
|
||||
fileData = await self._getFileData(document)
|
||||
|
||||
with io.BytesIO(fileData) as imgStream:
|
||||
img = Image.open(imgStream)
|
||||
metadata = ContentMetadata(
|
||||
|
|
@ -336,7 +346,7 @@ class DocumentProcessor:
|
|||
width=img.width,
|
||||
height=img.height,
|
||||
colorMode=img.mode,
|
||||
mimeType=document.mimeType,
|
||||
mimeType=mimeType,
|
||||
base64Encoded=True
|
||||
)
|
||||
|
||||
|
|
@ -353,15 +363,13 @@ class DocumentProcessor:
|
|||
logger.error(f"Error processing image document: {str(e)}")
|
||||
raise FileProcessingError(f"Failed to process image document: {str(e)}")
|
||||
|
||||
async def _processPdf(self, document: ChatDocument) -> List[ContentItem]:
|
||||
async def _processPdf(self, fileData: bytes, filename: str, mimeType: str) -> List[ContentItem]:
|
||||
"""Process PDF document"""
|
||||
try:
|
||||
self._loadPdfExtractor()
|
||||
if not pdfExtractorLoaded:
|
||||
raise FileProcessingError("PDF extraction libraries not available")
|
||||
|
||||
fileData = await self._getFileData(document)
|
||||
|
||||
contentItems = []
|
||||
|
||||
with io.BytesIO(fileData) as pdfStream:
|
||||
|
|
@ -424,15 +432,13 @@ class DocumentProcessor:
|
|||
logger.error(f"Error processing PDF document: {str(e)}")
|
||||
raise FileProcessingError(f"Failed to process PDF document: {str(e)}")
|
||||
|
||||
async def _processDocx(self, document: ChatDocument) -> List[ContentItem]:
|
||||
async def _processDocx(self, fileData: bytes, filename: str, mimeType: str) -> List[ContentItem]:
|
||||
"""Process Word document"""
|
||||
try:
|
||||
self._loadOfficeExtractor()
|
||||
if not officeExtractorLoaded:
|
||||
raise FileProcessingError("Office extraction libraries not available")
|
||||
|
||||
fileData = await self._getFileData(document)
|
||||
|
||||
with io.BytesIO(fileData) as docxStream:
|
||||
doc = docx.Document(docxStream)
|
||||
|
||||
|
|
@ -465,15 +471,13 @@ class DocumentProcessor:
|
|||
logger.error(f"Error processing Word document: {str(e)}")
|
||||
raise FileProcessingError(f"Failed to process Word document: {str(e)}")
|
||||
|
||||
async def _processXlsx(self, document: ChatDocument) -> List[ContentItem]:
|
||||
async def _processXlsx(self, fileData: bytes, filename: str, mimeType: str) -> List[ContentItem]:
|
||||
"""Process Excel document"""
|
||||
try:
|
||||
self._loadOfficeExtractor()
|
||||
if not officeExtractorLoaded:
|
||||
raise FileProcessingError("Office extraction libraries not available")
|
||||
|
||||
fileData = await self._getFileData(document)
|
||||
|
||||
contentItems = []
|
||||
|
||||
with io.BytesIO(fileData) as xlsxStream:
|
||||
|
|
@ -509,17 +513,15 @@ class DocumentProcessor:
|
|||
logger.error(f"Error processing Excel document: {str(e)}")
|
||||
raise FileProcessingError(f"Failed to process Excel document: {str(e)}")
|
||||
|
||||
async def _processBinary(self, document: ChatDocument) -> List[ContentItem]:
|
||||
async def _processBinary(self, fileData: bytes, filename: str, mimeType: str) -> List[ContentItem]:
|
||||
"""Process binary document"""
|
||||
try:
|
||||
fileData = await self._getFileData(document)
|
||||
|
||||
return [ContentItem(
|
||||
label="binary",
|
||||
data=base64.b64encode(fileData).decode('utf-8'),
|
||||
metadata=ContentMetadata(
|
||||
size=len(fileData),
|
||||
mimeType=document.mimeType,
|
||||
mimeType=mimeType,
|
||||
base64Encoded=True,
|
||||
error="Unsupported file type"
|
||||
)
|
||||
|
|
@ -577,13 +579,9 @@ class DocumentProcessor:
|
|||
Return ONLY the extracted information in a clear, concise format.
|
||||
"""
|
||||
|
||||
# Get AI response
|
||||
response = await self.interfaceComponent.callAi([
|
||||
{"role": "system", "content": "You are an expert at extracting relevant information from documents."},
|
||||
{"role": "user", "content": aiPrompt}
|
||||
])
|
||||
|
||||
chunkResults.append(response.strip())
|
||||
# Note: This would need to be implemented with actual AI service
|
||||
# For now, just return the original content
|
||||
chunkResults.append(contentToProcess)
|
||||
|
||||
# Combine chunk results
|
||||
combinedResult = "\n".join(chunkResults)
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ from modules.interfaces.interfaceChatModel import (
|
|||
TaskStatus, ChatDocument, TaskItem, TaskAction, TaskResult,
|
||||
ChatStat, ChatLog, ChatMessage, ChatWorkflow
|
||||
)
|
||||
from modules.interfaces.interfaceAiCalls import interfaceAiCalls
|
||||
from modules.interfaces.interfaceAiCalls import AiCalls
|
||||
from modules.interfaces.interfaceChatObjects import getInterface as getChatObjects
|
||||
from modules.interfaces.interfaceChatModel import ActionResult
|
||||
from modules.interfaces.interfaceComponentObjects import getInterface as getComponentObjects
|
||||
from modules.workflow.managerDocument import DocumentManager
|
||||
from modules.methods.methodBase import MethodBase
|
||||
|
|
@ -32,7 +33,7 @@ class ServiceContainer:
|
|||
# Initialize managers
|
||||
self.interfaceChat = getChatObjects(currentUser)
|
||||
self.interfaceComponent = getComponentObjects(currentUser)
|
||||
self.interfaceAiCalls = interfaceAiCalls()
|
||||
self.interfaceAiCalls = AiCalls()
|
||||
self.documentManager = DocumentManager(self)
|
||||
|
||||
# Initialize methods catalog
|
||||
|
|
@ -104,7 +105,11 @@ class ServiceContainer:
|
|||
|
||||
def extractContent(self, prompt: str, document: ChatDocument) -> str:
|
||||
"""Extract content from document using prompt"""
|
||||
return self.documentManager.extractContent(prompt, document)
|
||||
return self.documentManager.extractContentFromDocument(prompt, document)
|
||||
|
||||
def extractContentFromFileData(self, prompt: str, fileData: bytes, filename: str, mimeType: str, base64Encoded: bool = False) -> str:
|
||||
"""Extract content from file data directly using prompt"""
|
||||
return self.documentManager.extractContentFromFileData(prompt, fileData, filename, mimeType, base64Encoded)
|
||||
|
||||
def getMethodsCatalog(self) -> Dict[str, Any]:
|
||||
"""Get catalog of available methods and their actions"""
|
||||
|
|
@ -417,7 +422,7 @@ Please provide a clear summary of this message."""
|
|||
mimeType=mimeType
|
||||
)
|
||||
|
||||
async def executeMethod(self, methodName: str, actionName: str, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
async def executeAction(self, methodName: str, actionName: str, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> ActionResult:
|
||||
"""Execute a method action"""
|
||||
try:
|
||||
if methodName not in self.methods:
|
||||
|
|
|
|||
|
|
@ -1,8 +1,21 @@
|
|||
INIT
|
||||
|
||||
conda activate poweron
|
||||
pip install -r requirements.txt
|
||||
cd gateway
|
||||
python app.py
|
||||
|
||||
|
||||
TODO
|
||||
- neutralizer to put back placeholders to the returned data
|
||||
- referenceHandling and authentication for connections in the method actions
|
||||
- check methods
|
||||
- test for workflow backend with userdata
|
||||
- prompt for task definition to fix
|
||||
- method definition list directly based on functions
|
||||
- neutralizer to put back placeholders to the returned data after ai
|
||||
|
||||
|
||||
|
||||
********************
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class MethodParameter(BaseModel):
|
|||
validation: Optional[callable] = None
|
||||
description: str
|
||||
|
||||
class MethodResult(BaseModel):
|
||||
class ActionResult(BaseModel):
|
||||
"""Model for method results"""
|
||||
success: bool
|
||||
data: Dict[str, Any]
|
||||
|
|
@ -67,7 +67,7 @@ class MethodBase:
|
|||
"""Available actions and their parameters"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def execute(self, action: str, parameters: Dict[str, Any], auth_data: Optional[Dict[str, Any]] = None) -> MethodResult:
|
||||
async def execute(self, action: str, parameters: Dict[str, Any], auth_data: Optional[Dict[str, Any]] = None) -> ActionResult:
|
||||
"""Execute method action with authentication data"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
|
|
|||
15
test_config.ini
Normal file
15
test_config.ini
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Test configuration for workflow testing
|
||||
DB_APP_HOST=_test_data_app
|
||||
DB_APP_DATABASE=app
|
||||
DB_APP_USER=test
|
||||
DB_APP_PASSWORD_SECRET=test123
|
||||
|
||||
DB_CHAT_HOST=_test_data_chat
|
||||
DB_CHAT_DATABASE=chat
|
||||
DB_CHAT_USER=test
|
||||
DB_CHAT_PASSWORD_SECRET=test123
|
||||
|
||||
# AI Configuration
|
||||
AI_PROVIDER=openai
|
||||
AI_MODEL=gpt-3.5-turbo
|
||||
AI_API_KEY_SECRET=test_key
|
||||
175
test_workflow.py
Normal file
175
test_workflow.py
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test routine for WorkflowManager.workflowProcess()
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime, UTC
|
||||
import uuid
|
||||
|
||||
# Set up test configuration
|
||||
os.environ['POWERON_CONFIG_FILE'] = 'test_config.ini'
|
||||
|
||||
# Simple imports from modules (same as app.py)
|
||||
from modules.interfaces.interfaceAppObjects import User, UserConnection
|
||||
from modules.interfaces.interfaceChatObjects import ChatObjects
|
||||
from modules.interfaces.interfaceChatModel import UserInputRequest, ChatWorkflow
|
||||
from modules.workflow.managerWorkflow import WorkflowManager
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout),
|
||||
logging.FileHandler('test_workflow.log')
|
||||
]
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def create_test_user() -> User:
|
||||
"""Create a test user for the workflow"""
|
||||
# Create test connections for Microsoft services
|
||||
connections = [
|
||||
UserConnection(
|
||||
id="conn-001",
|
||||
authority="microsoft",
|
||||
name="Test Microsoft Account",
|
||||
enabled=True,
|
||||
accessToken="test-token-123",
|
||||
refreshToken="test-refresh-456",
|
||||
expiresAt=datetime.now(UTC).isoformat(),
|
||||
scopes=["Files.ReadWrite", "Mail.ReadWrite", "Sites.ReadWrite.All"]
|
||||
)
|
||||
]
|
||||
|
||||
return User(
|
||||
id="test-user-001",
|
||||
mandateId="test-mandate-001",
|
||||
username="testuser",
|
||||
email="test@example.com",
|
||||
fullName="Test User",
|
||||
enabled=True,
|
||||
language="en",
|
||||
privilege="user",
|
||||
authenticationAuthority="local",
|
||||
connections=connections
|
||||
)
|
||||
|
||||
def create_test_workflow() -> ChatWorkflow:
|
||||
"""Create a test workflow"""
|
||||
return ChatWorkflow(
|
||||
id="test-workflow-001",
|
||||
mandateId="test-mandate-001",
|
||||
status="running",
|
||||
name="Business Intelligence Analysis Workflow",
|
||||
currentRound=1,
|
||||
lastActivity=datetime.now(UTC).isoformat(),
|
||||
startedAt=datetime.now(UTC).isoformat(),
|
||||
logs=[],
|
||||
messages=[],
|
||||
stats=None,
|
||||
tasks=[]
|
||||
)
|
||||
|
||||
def create_test_user_input() -> UserInputRequest:
|
||||
"""Create test user input with a meaningful business intelligence task"""
|
||||
return UserInputRequest(
|
||||
prompt="""Please analyze the quarterly sales data and create a comprehensive business intelligence report.
|
||||
|
||||
The task involves:
|
||||
1. Extract and analyze sales data from the provided Excel files
|
||||
2. Identify key trends, patterns, and anomalies in the data
|
||||
3. Create visualizations (charts and graphs) to illustrate findings
|
||||
4. Generate a professional PowerPoint presentation summarizing the analysis
|
||||
5. Create a detailed markdown report with actionable insights
|
||||
6. Search for industry benchmarks and best practices to compare our performance
|
||||
7. Store the final reports in SharePoint for team access
|
||||
|
||||
Please ensure the analysis includes:
|
||||
- Sales performance by region and product category
|
||||
- Month-over-month growth trends
|
||||
- Customer segmentation analysis
|
||||
- Revenue forecasting for the next quarter
|
||||
- Recommendations for improving sales performance
|
||||
|
||||
The output should be suitable for executive review and include both high-level summaries and detailed technical analysis.""",
|
||||
listFileId=["sales_data_q1.xlsx", "sales_data_q2.xlsx", "customer_data.csv"],
|
||||
userLanguage="en"
|
||||
)
|
||||
|
||||
async def test_workflow_process():
|
||||
"""Test the workflowProcess function"""
|
||||
try:
|
||||
logger.info("Starting workflow process test...")
|
||||
|
||||
# Create test data
|
||||
test_user = create_test_user()
|
||||
test_workflow = create_test_workflow()
|
||||
test_user_input = create_test_user_input()
|
||||
|
||||
logger.info(f"Test user: {test_user.username}")
|
||||
logger.info(f"Test workflow: {test_workflow.id}")
|
||||
logger.info(f"Test input prompt: {test_user_input.prompt[:100]}...")
|
||||
logger.info(f"Test files: {test_user_input.listFileId}")
|
||||
|
||||
# Initialize ChatObjects interface
|
||||
chat_interface = ChatObjects(test_user)
|
||||
logger.info("ChatObjects interface initialized")
|
||||
|
||||
# Initialize WorkflowManager
|
||||
workflow_manager = WorkflowManager(chat_interface, test_user)
|
||||
logger.info("WorkflowManager initialized")
|
||||
|
||||
# Test the workflowProcess function
|
||||
logger.info("Calling workflowProcess...")
|
||||
task = await workflow_manager.workflowProcess(test_user_input, test_workflow)
|
||||
|
||||
# Log results
|
||||
if task:
|
||||
logger.info("✅ Task created successfully!")
|
||||
logger.info(f"Task ID: {task.id}")
|
||||
logger.info(f"Task Status: {task.status}")
|
||||
logger.info(f"Task Feedback: {task.feedback}")
|
||||
logger.info(f"Number of actions: {len(task.actionList) if task.actionList else 0}")
|
||||
|
||||
if task.actionList:
|
||||
for i, action in enumerate(task.actionList):
|
||||
logger.info(f"Action {i+1}: {action.execMethod}.{action.execAction}")
|
||||
logger.info(f" Parameters: {action.execParameters}")
|
||||
else:
|
||||
logger.warning("⚠️ No task was created")
|
||||
|
||||
logger.info("Test completed successfully!")
|
||||
return task
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Test failed with error: {str(e)}")
|
||||
logger.exception("Full traceback:")
|
||||
raise
|
||||
|
||||
async def main():
|
||||
"""Main function to run the test"""
|
||||
logger.info("=" * 50)
|
||||
logger.info("BUSINESS INTELLIGENCE WORKFLOW TEST")
|
||||
logger.info("=" * 50)
|
||||
|
||||
try:
|
||||
task = await test_workflow_process()
|
||||
logger.info("=" * 50)
|
||||
logger.info("TEST COMPLETED SUCCESSFULLY")
|
||||
logger.info("=" * 50)
|
||||
return task
|
||||
except Exception as e:
|
||||
logger.error("=" * 50)
|
||||
logger.error("TEST FAILED")
|
||||
logger.error("=" * 50)
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the test
|
||||
asyncio.run(main())
|
||||
Loading…
Reference in a new issue