From 1ceb361274707648ccecbe83911a746720aff0b2 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Sat, 10 May 2025 21:36:18 +0200
Subject: [PATCH] prod azure 1.1.0 fix documents
---
connectors/BACKUP-connectorDbJson.py | 569 ---------
modules/agentAnalyst.py | 436 ++++---
modules/agentEmail.py | 171 ++-
modules/agentWebcrawler.py | 37 +-
modules/documentProcessor.py | 261 +++-
modules/lucydomInterface.py | 24 +-
modules/lucydomModel.py | 3 +-
modules/workflowAgentsRegistry.py | 68 +-
modules/workflowManager.py | 244 ++--
notes/changelog.txt | 16 +-
static/10_email_preview.html | 42 -
static/11_email_template.json | 6 -
static/12_email_preview.html | 42 -
static/13_email_template.json | 6 -
static/14_microsoft_authentication.html | 47 -
static/15_microsoft_authentication.html | 28 -
static/16_email_preview.html | 42 -
static/17_email_template.json | 6 -
static/18_generated_code.py | 48 -
static/19_execution_history.json | 19 -
static/1_LF-Details.png | Bin 253009 -> 0 bytes
static/20_prime_numbers.csv | 1000 ----------------
static/21_email_preview.html | 42 -
static/22_email_template.json | 6 -
static/23_documentProcessor.py | 933 ---------------
static/24_defAttributes.py | 123 --
static/25_email_preview.html | 42 -
static/26_email_template.json | 6 -
static/27_email_preview.html | 42 -
static/28_email_template.json | 6 -
static/29_email_preview.html | 42 -
static/2_LF-Details_Description.txt | 295 -----
static/30_email_template.json | 6 -
static/31_email_preview.html | 42 -
static/32_email_template.json | 6 -
static/33_email_preview.html | 42 -
static/34_email_template.json | 6 -
static/35_email_preview.html | 42 -
static/36_email_template.json | 6 -
static/37_gatewayInterface.py | 522 --------
static/38_email_preview.html | 49 -
static/39_email_template.json | 6 -
static/3_generated_code.py | 38 -
static/40_email_preview.html | 42 -
static/41_email_template.json | 6 -
static/42_gatewayInterface.py | 526 --------
static/43_defAttributes.py | 123 --
static/44_email_preview.html | 42 -
static/45_email_template.json | 6 -
static/46_email_preview.html | 42 -
static/47_email_template.json | 6 -
static/48_email_preview.html | 42 -
static/49_email_template.json | 6 -
static/4_execution_history.json | 19 -
static/50_LF-Nutshell.png | Bin 52108 -> 0 bytes
static/51_PowerOn NDA 2025.pdf | Bin 39612 -> 0 bytes
static/52_email_preview.html | 42 -
static/53_email_template.json | 6 -
static/5_prime_numbers.txt | 1000 ----------------
static/6_email_preview.html | 42 -
static/7_email_template.json | 6 -
static/8_email_preview.html | 74 --
static/9_email_template.json | 6 -
.../7d08aab9-a170-4975-8898-bc7e0a95488e.json | 1 -
tool_testBackendSingle.py | 432 -------
tool_testData.py | 1064 -----------------
tool_testUser.py | 244 ----
67 files changed, 802 insertions(+), 8392 deletions(-)
delete mode 100644 connectors/BACKUP-connectorDbJson.py
delete mode 100644 static/10_email_preview.html
delete mode 100644 static/11_email_template.json
delete mode 100644 static/12_email_preview.html
delete mode 100644 static/13_email_template.json
delete mode 100644 static/14_microsoft_authentication.html
delete mode 100644 static/15_microsoft_authentication.html
delete mode 100644 static/16_email_preview.html
delete mode 100644 static/17_email_template.json
delete mode 100644 static/18_generated_code.py
delete mode 100644 static/19_execution_history.json
delete mode 100644 static/1_LF-Details.png
delete mode 100644 static/20_prime_numbers.csv
delete mode 100644 static/21_email_preview.html
delete mode 100644 static/22_email_template.json
delete mode 100644 static/23_documentProcessor.py
delete mode 100644 static/24_defAttributes.py
delete mode 100644 static/25_email_preview.html
delete mode 100644 static/26_email_template.json
delete mode 100644 static/27_email_preview.html
delete mode 100644 static/28_email_template.json
delete mode 100644 static/29_email_preview.html
delete mode 100644 static/2_LF-Details_Description.txt
delete mode 100644 static/30_email_template.json
delete mode 100644 static/31_email_preview.html
delete mode 100644 static/32_email_template.json
delete mode 100644 static/33_email_preview.html
delete mode 100644 static/34_email_template.json
delete mode 100644 static/35_email_preview.html
delete mode 100644 static/36_email_template.json
delete mode 100644 static/37_gatewayInterface.py
delete mode 100644 static/38_email_preview.html
delete mode 100644 static/39_email_template.json
delete mode 100644 static/3_generated_code.py
delete mode 100644 static/40_email_preview.html
delete mode 100644 static/41_email_template.json
delete mode 100644 static/42_gatewayInterface.py
delete mode 100644 static/43_defAttributes.py
delete mode 100644 static/44_email_preview.html
delete mode 100644 static/45_email_template.json
delete mode 100644 static/46_email_preview.html
delete mode 100644 static/47_email_template.json
delete mode 100644 static/48_email_preview.html
delete mode 100644 static/49_email_template.json
delete mode 100644 static/4_execution_history.json
delete mode 100644 static/50_LF-Nutshell.png
delete mode 100644 static/51_PowerOn NDA 2025.pdf
delete mode 100644 static/52_email_preview.html
delete mode 100644 static/53_email_template.json
delete mode 100644 static/5_prime_numbers.txt
delete mode 100644 static/6_email_preview.html
delete mode 100644 static/7_email_template.json
delete mode 100644 static/8_email_preview.html
delete mode 100644 static/9_email_template.json
delete mode 100644 token_storage/7d08aab9-a170-4975-8898-bc7e0a95488e.json
delete mode 100644 tool_testBackendSingle.py
delete mode 100644 tool_testData.py
delete mode 100644 tool_testUser.py
diff --git a/connectors/BACKUP-connectorDbJson.py b/connectors/BACKUP-connectorDbJson.py
deleted file mode 100644
index f4bdea80..00000000
--- a/connectors/BACKUP-connectorDbJson.py
+++ /dev/null
@@ -1,569 +0,0 @@
-import json
-import os
-from typing import List, Dict, Any, Optional, Union
-import logging
-
-logger = logging.getLogger(__name__)
-
-class DatabaseConnector:
- """
- A connector for JSON-based data storage.
- Provides generic database operations with tenant and user context support.
- """
- def __init__(self, dbHost: str, dbDatabase: str, dbUser: str = None, dbPassword: str = None,
- mandateId: int = None, userId: int = None, skipInitialIdLookup: bool = False):
- """
- Initializes the JSON database connector.
-
- Args:
- dbHost: Directory for the JSON files
- dbDatabase: Database name
- dbUser: Username for authentication (optional)
- dbPassword: API key for authentication (optional)
- mandateId: Context parameter for the tenant
- userId: Context parameter for the user
- skipInitialIdLookup: When True, skips looking up initial IDs for mandateId and userId
- """
- # Store the input parameters
- self.dbHost = dbHost
- self.dbDatabase = dbDatabase
- self.dbUser = dbUser
- self.dbPassword = dbPassword
- self.skipInitialIdLookup = skipInitialIdLookup
-
- # Check if context parameters are set
- if mandateId is None or userId is None:
- raise ValueError("mandateId and userId must be set")
-
- # Ensure the database directory exists
- self.dbFolder = os.path.join(self.dbHost, self.dbDatabase)
- os.makedirs(self.dbFolder, exist_ok=True)
-
- # Cache for loaded data
- self._tablesCache = {}
-
- # Initialize system table
- self._systemTableName = "_system"
- self._initializeSystemTable()
-
- # Temporarily store mandateId and userId
- self._mandateId = mandateId
- self._userId = userId
-
- # If mandateId or userId are 0 and we're not skipping ID lookup, try to use the initial IDs
- if not skipInitialIdLookup:
- if mandateId == 0:
- initialMandateId = self.getInitialId("mandates")
- if initialMandateId is not None:
- self._mandateId = initialMandateId
- logger.info(f"Using initial mandateId: {initialMandateId} instead of 0")
-
- if userId == 0:
- initialUserId = self.getInitialId("users")
- if initialUserId is not None:
- self._userId = initialUserId
- logger.info(f"Using initial userId: {initialUserId} instead of 0")
-
- # Set the effective IDs as properties
- self.mandateId = self._mandateId
- self.userId = self._userId
-
- logger.info(f"DatabaseConnector initialized for directory: {self.dbFolder}")
- logger.debug(f"Context: mandateId={self.mandateId}, userId={self.userId}")
-
- def _initializeSystemTable(self):
- """Initializes the system table if it doesn't exist yet."""
- systemTablePath = self._getTablePath(self._systemTableName)
- if not os.path.exists(systemTablePath):
- emptySystemTable = {}
- self._saveSystemTable(emptySystemTable)
- logger.info(f"System table initialized in {systemTablePath}")
- else:
- # Load existing system table to ensure it's available
- self._loadSystemTable()
- logger.debug(f"Existing system table loaded from {systemTablePath}")
-
- def _loadSystemTable(self) -> Dict[str, int]:
- """Loads the system table with the initial IDs."""
- # Check if system table is in cache
- if f"_{self._systemTableName}" in self._tablesCache:
- return self._tablesCache[f"_{self._systemTableName}"]
-
- systemTablePath = self._getTablePath(self._systemTableName)
- try:
- if os.path.exists(systemTablePath):
- with open(systemTablePath, 'r', encoding='utf-8') as f:
- data = json.load(f)
- # Store in cache with special prefix to avoid collision with regular tables
- self._tablesCache[f"_{self._systemTableName}"] = data
- return data
- else:
- self._tablesCache[f"_{self._systemTableName}"] = {}
- return {}
- except Exception as e:
- logger.error(f"Error loading the system table: {e}")
- self._tablesCache[f"_{self._systemTableName}"] = {}
- return {}
-
- def _saveSystemTable(self, data: Dict[str, int]) -> bool:
- """Saves the system table with the initial IDs."""
- systemTablePath = self._getTablePath(self._systemTableName)
- try:
- with open(systemTablePath, 'w', encoding='utf-8') as f:
- json.dump(data, f, indent=2, ensure_ascii=False)
- # Update cache
- self._tablesCache[f"_{self._systemTableName}"] = data
- return True
- except Exception as e:
- logger.error(f"Error saving the system table: {e}")
- return False
-
- def _getTablePath(self, table: str) -> str:
- """Returns the full path to a table file"""
- return os.path.join(self.dbFolder, f"{table}.json")
-
- def _loadTable(self, table: str) -> List[Dict[str, Any]]:
- """Loads a table from the corresponding JSON file"""
- path = self._getTablePath(table)
-
- # If the table is the system table, load it directly
- if table == self._systemTableName:
- return [] # The system table is not treated like normal tables
-
- # If the table is already in the cache, use the cache
- if table in self._tablesCache:
- return self._tablesCache[table]
-
- # Otherwise load the file
- try:
- if os.path.exists(path):
- with open(path, 'r', encoding='utf-8') as f:
- data = json.load(f)
- self._tablesCache[table] = data
-
- # If data was loaded and no initial ID is registered yet,
- # register the ID of the first record (if available)
- if data and not self.hasInitialId(table):
- if "id" in data[0]:
- self._registerInitialId(table, data[0]["id"])
- logger.info(f"Initial ID {data[0]['id']} for table {table} retroactively registered")
-
- return data
- else:
- # If the file doesn't exist, create an empty table
- logger.info(f"New table {table}")
- self._tablesCache[table] = []
- self._saveTable(table, [])
- return []
- except Exception as e:
- logger.error(f"Error loading table {table}: {e}")
- return []
-
- def _saveTable(self, table: str, data: List[Dict[str, Any]]) -> bool:
- """Saves a table to the corresponding JSON file"""
- # The system table is handled specially
- if table == self._systemTableName:
- return False
-
- path = self._getTablePath(table)
- try:
- with open(path, 'w', encoding='utf-8') as f:
- json.dump(data, f, indent=2, ensure_ascii=False)
-
- # Update the cache
- self._tablesCache[table] = data
- return True
- except Exception as e:
- logger.error(f"Error saving table {table}: {e}")
- return False
-
- def _filterByContext(self, records: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
- """
- Filters records by tenant and user context,
- if these fields exist in the record.
- """
- filteredRecords = []
-
- for record in records:
- # Check if mandateId exists in the record and is not null
- hasMandate = "mandateId" in record and record["mandateId"] is not None and record["mandateId"] != ""
-
- # Check if userId exists in the record and is not null
- hasUser = "userId" in record and record["userId"] is not None and record["userId"] != ""
-
- # If both exist, filter accordingly
- if hasMandate and hasUser:
- if record["mandateId"] == self.mandateId:
- filteredRecords.append(record)
- # If only mandateId exists
- elif hasMandate and not hasUser:
- if record["mandateId"] == self.mandateId:
- filteredRecords.append(record)
- # If neither mandateId nor userId exist, add the record
- elif not hasMandate and not hasUser:
- filteredRecords.append(record)
-
- return filteredRecords
-
-
- def _applyRecordFilter(self, records: List[Dict[str, Any]], recordFilter: Dict[str, Any] = None) -> List[Dict[str, Any]]:
- """Applies a record filter to the records"""
- if not recordFilter:
- return records
-
- filteredRecords = []
-
- for record in records:
- match = True
-
- for field, value in recordFilter.items():
- # Check if the field exists
- if field not in record:
- match = False
- break
-
- # Handle type conversion for integer comparisons both ways
- if isinstance(value, int) and isinstance(record[field], str) and record[field].isdigit():
- # Filter value is int, record value is string
- if value != int(record[field]):
- match = False
- break
- elif isinstance(value, str) and value.isdigit() and isinstance(record[field], int):
- # Filter value is string, record value is int
- if record[field] != int(value):
- match = False
- break
- # Otherwise direct comparison
- elif record[field] != value:
- match = False
- break
-
- if match:
- filteredRecords.append(record)
-
- return filteredRecords
-
- def _registerInitialId(self, table: str, initialId: int) -> bool:
- """
- Registers the initial ID for a table.
-
- Args:
- table: Name of the table
- initialId: The initial ID
-
- Returns:
- True on success, False on error
- """
- try:
- # Load the current system table
- systemData = self._loadSystemTable()
-
- # Only register if not already present
- if table not in systemData:
- systemData[table] = initialId
- success = self._saveSystemTable(systemData)
- if success:
- logger.info(f"Initial ID {initialId} for table {table} registered")
- return success
- return True # If already present, this is not an error
- except Exception as e:
- logger.error(f"Error registering the initial ID for table {table}: {e}")
- return False
-
- def _removeInitialId(self, table: str) -> bool:
- """
- Removes the initial ID for a table from the system table.
-
- Args:
- table: Name of the table
-
- Returns:
- True on success, False on error
- """
- try:
- # Load the current system table
- systemData = self._loadSystemTable()
-
- # Remove the entry if it exists
- if table in systemData:
- del systemData[table]
- success = self._saveSystemTable(systemData)
- if success:
- logger.info(f"Initial ID for table {table} removed from system table")
- return success
- return True # If not present, this is not an error
- except Exception as e:
- logger.error(f"Error removing initial ID for table {table}: {e}")
- return False
-
- # Public API
-
- def getTables(self) -> List[str]:
- """
- Returns a list of all available tables.
-
- Returns:
- List of table names
- """
- tables = []
-
- try:
- for filename in os.listdir(self.dbFolder):
- if filename.endswith('.json') and not filename.startswith('_'):
- tableName = filename[:-5] # Remove the .json extension
- tables.append(tableName)
- except Exception as e:
- logger.error(f"Error reading the database directory: {e}")
-
- return tables
-
- def getFields(self, table: str) -> List[str]:
- """
- Returns a list of all fields in a table.
-
- Args:
- table: Name of the table
-
- Returns:
- List of field names
- """
- # Load the table data
- data = self._loadTable(table)
-
- if not data:
- return []
-
- # Take the first record as a reference for the fields
- fields = list(data[0].keys()) if data else []
-
- return fields
-
- def getSchema(self, table: str, language: str = None) -> Dict[str, Dict[str, Any]]:
- """
- Returns a schema object for a table with data types and labels.
-
- Args:
- table: Name of the table
- language: Language for the labels (optional)
-
- Returns:
- Schema object with fields, data types and labels
- """
- # Load the table data
- data = self._loadTable(table)
-
- schema = {}
-
- if not data:
- return schema
-
- # Take the first record as a reference for the fields and data types
- firstRecord = data[0]
-
- for field, value in firstRecord.items():
- # Determine the data type
- dataType = type(value).__name__
-
- # Create label (default is the field name)
- label = field
-
- schema[field] = {
- "type": dataType,
- "label": label
- }
-
- return schema
-
- def getRecordset(self, table: str, fieldFilter: List[str] = None, recordFilter: Dict[str, Any] = None) -> List[Dict[str, Any]]:
- """
- Returns a list of records from a table, filtered by criteria.
-
- Args:
- table: Name of the table
- fieldFilter: Filter for fields (which fields should be returned)
- recordFilter: Filter for records (which records should be returned)
-
- Returns:
- List of filtered records
- """
- # Load the table data
- data = self._loadTable(table)
- logger.debug(f"getRecordset: data volume of {len(data)} bytes")
-
- # Filter by tenant and user context
- filteredData = self._filterByContext(data)
-
- # Apply recordFilter if available
- if recordFilter:
- filteredData = self._applyRecordFilter(filteredData, recordFilter)
-
- # If fieldFilter is available, reduce the fields
- if fieldFilter and isinstance(fieldFilter, list):
- result = []
- for record in filteredData:
- filteredRecord = {}
- for field in fieldFilter:
- if field in record:
- filteredRecord[field] = record[field]
- result.append(filteredRecord)
- return result
-
- return filteredData
-
- def recordCreate(self, table: str, recordData: Dict[str, Any]) -> Dict[str, Any]:
- """
- Creates a new record in the table.
-
- Args:
- table: Name of the table
- recordData: Data for the new record
-
- Returns:
- The created record
- """
- # Load the table data
- data = self._loadTable(table)
-
- # Add mandateId and userId if not present or 0
- if "mandateId" not in recordData or recordData["mandateId"] == 0:
- recordData["mandateId"] = self.mandateId
-
- if "userId" not in recordData or recordData["userId"] == 0:
- recordData["userId"] = self.userId
-
- # Determine the next ID if not present
- if "id" not in recordData:
- nextId = 1
- if data:
- nextId = max(record["id"] for record in data if "id" in record) + 1
- recordData["id"] = nextId
-
- # If the table is empty and a system ID should be registered
- if not data:
- self._registerInitialId(table, recordData["id"])
- logger.info(f"Initial ID {recordData['id']} for table {table} has been registered")
-
- # Add the new record
- data.append(recordData)
-
- # Save the updated table
- if self._saveTable(table, data):
- return recordData
- else:
- raise ValueError(f"Error creating the record in table {table}")
-
- def recordDelete(self, table: str, recordId: Union[str, int]) -> bool:
- """
- Deletes a record from the table.
-
- Args:
- table: Name of the table
- recordId: ID of the record to delete
-
- Returns:
- True on success, False on error
- """
- # Load table data
- data = self._loadTable(table)
-
- # Search for the record
- for i, record in enumerate(data):
- if "id" in record and record["id"] == recordId:
- # Check if the record belongs to the current mandate
- if "mandateId" in record and record["mandateId"] != self.mandateId:
- raise ValueError("Not your mandate")
-
- # Check if it's an initial record
- initialId = self.getInitialId(table)
- if initialId is not None and initialId == recordId:
- # Remove this entry from the system table
- self._removeInitialId(table)
- logger.info(f"Initial ID {recordId} for table {table} has been removed from the system table")
-
- # Delete the record
- del data[i]
-
- # Save the updated table
- return self._saveTable(table, data)
-
- # Record not found
- return False
-
- def recordModify(self, table: str, recordId: Union[str, int], recordData: Dict[str, Any]) -> Dict[str, Any]:
- """
- Modifies a record in the table.
-
- Args:
- table: Name of the table
- recordId: ID of the record to modify
- recordData: New data for the record
-
- Returns:
- The updated record
- """
- # Load table data
- data = self._loadTable(table)
-
- # Search for the record
- for i, record in enumerate(data):
- if "id" in record and record["id"] == recordId:
- # Check if the record belongs to the current mandate
- if "mandateId" in record and record["mandateId"] != self.mandateId:
- raise ValueError("Not your mandate")
-
- # Prevent changing the ID
- if "id" in recordData and recordData["id"] != recordId:
- raise ValueError(f"The ID of a record in table {table} cannot be changed")
-
- # Update the record
- for key, value in recordData.items():
- data[i][key] = value
-
- # Save the updated table
- if self._saveTable(table, data):
- return data[i]
- else:
- raise ValueError(f"Error updating record in table {table}")
-
- # Record not found
- raise ValueError(f"Record with ID {recordId} not found in table {table}")
-
- def hasInitialId(self, table: str) -> bool:
- """
- Checks if an initial ID is registered for a table.
-
- Args:
- table: Name of the table
-
- Returns:
- True if an initial ID is registered, otherwise False
- """
- systemData = self._loadSystemTable()
- return table in systemData
-
- def getInitialId(self, table: str) -> Optional[int]:
- """
- Returns the initial ID for a table.
-
- Args:
- table: Name of the table
-
- Returns:
- The initial ID or None if not present
- """
- systemData = self._loadSystemTable()
- initialId = systemData.get(table)
- logger.debug(f"Database '{self.dbDatabase}': Initial ID for table '{table}' is {initialId}")
- if initialId is None:
- logger.debug(f"No initial ID found for table {table}")
- return initialId
-
- def getAllInitialIds(self) -> Dict[str, int]:
- """
- Returns all registered initial IDs.
-
- Returns:
- Dictionary with table names as keys and initial IDs as values
- """
- systemData = self._loadSystemTable()
- return systemData.copy() # Return a copy to protect the original
\ No newline at end of file
diff --git a/modules/agentAnalyst.py b/modules/agentAnalyst.py
index 5e68c91e..b967af5f 100644
--- a/modules/agentAnalyst.py
+++ b/modules/agentAnalyst.py
@@ -64,9 +64,11 @@ class AgentAnalyst(AgentBase):
}
# Extract data from documents - focusing only on dataExtracted
+ self.mydom.logAdd(task["workflowId"], "Extracting data from documents...", level="info", progress=35)
datasets, documentContext = self._extractData(inputDocuments)
# Generate task analysis to understand what's needed
+ self.mydom.logAdd(task["workflowId"], "Analyzing task requirements...", level="info", progress=45)
analysisPlan = await self._analyzeTask(prompt, documentContext, datasets, outputSpecs)
# Generate all required output documents
@@ -77,7 +79,11 @@ class AgentAnalyst(AgentBase):
outputSpecs = []
# Process each output specification
- for spec in outputSpecs:
+ totalSpecs = len(outputSpecs)
+ for i, spec in enumerate(outputSpecs):
+ progress = 45 + int((i / totalSpecs) * 45) # Progress from 45% to 90%
+ self.mydom.logAdd(task["workflowId"], f"Creating output {i+1}/{totalSpecs}...", level="info", progress=progress)
+
outputLabel = spec.get("label", "")
outputDescription = spec.get("description", "")
@@ -106,9 +112,9 @@ class AgentAnalyst(AgentBase):
documents.append(document)
# Generate feedback
- feedback = f"{analysisPlan.get('analysisApproach')}"
- if analysisPlan.get("keyInsights"):
- feedback += f"\n\n{analysisPlan.get('keyInsights')}"
+ feedback = f"{analysisPlan.get('feedback')}"
+ if analysisPlan.get("insights"):
+ feedback += f"\n\n{analysisPlan.get('insights')}"
return {
"feedback": feedback,
@@ -196,69 +202,74 @@ class AgentAnalyst(AgentBase):
return datasets, documentContext
- async def _analyzeTask(self, prompt: str, context: str, datasets: Dict, outputSpecs: List) -> Dict:
+ async def _analyzeTask(self, prompt: str, documentContext: str, datasets: Dict[str, Any], outputSpecs: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
- Use AI to analyze the task and create a plan for analysis.
+ Analyze the task requirements using AI.
Args:
prompt: The task prompt
- context: Document context text
- datasets: Dictionary of extracted datasets
+ documentContext: Context from input documents
+ datasets: Available datasets
outputSpecs: Output specifications
Returns:
Analysis plan dictionary
"""
- # Prepare dataset information
- datasetInfo = {}
- for name, df in datasets.items():
- try:
- datasetInfo[name] = {
- "shape": df.shape,
- "columns": df.columns.tolist(),
- "dtypes": {col: str(df[col].dtype) for col in df.columns},
- "sample": df.head(3).to_dict(orient='records')
- }
- except:
- datasetInfo[name] = {"error": "Could not process dataset"}
-
+ # Create analysis prompt
analysisPrompt = f"""
- Analyze this data analysis task and create a plan.
+ Analyze this data analysis task and create a detailed plan:
TASK: {prompt}
- AVAILABLE DATA:
- {json.dumps(datasetInfo, indent=2)}
-
DOCUMENT CONTEXT:
- {context[:1000]}... (truncated)
+ {documentContext}
- OUTPUT REQUIREMENTS:
+ AVAILABLE DATASETS:
+ {json.dumps(datasets, indent=2)}
+
+ REQUIRED OUTPUTS:
{json.dumps(outputSpecs, indent=2)}
- Create a detailed analysis plan in JSON format with the following structure:
+ Create a detailed analysis plan in JSON format with:
{{
- "analysisType": "statistical|trend|comparative|predictive|cluster|general",
- "keyQuestions": ["question1", "question2"],
- "recommendedVisualizations": [{{
- "type": "chart_type",
- "dataSource": "dataset_name",
- "variables": ["col1", "col2"],
- "purpose": "explanation"
- }}],
- "keyInsights": "brief summary of initial insights",
- "analysisApproach": "brief description of recommended approach"
+ "analysisSteps": [
+ {{
+ "step": "step description",
+ "purpose": "why this step is needed",
+ "datasets": ["dataset1", "dataset2"],
+ "techniques": ["technique1", "technique2"],
+ "outputs": ["output1", "output2"]
+ }}
+ ],
+ "visualizations": [
+ {{
+ "type": "visualization type",
+ "purpose": "what it shows",
+ "datasets": ["dataset1"],
+ "settings": {{"key": "value"}}
+ }}
+ ],
+ "insights": [
+ {{
+ "type": "insight type",
+ "description": "what to look for",
+ "datasets": ["dataset1"]
+ }}
+ ],
+ "feedback": "explanation of the analysis approach"
}}
- Only return valid JSON. No preamble or explanations.
+ Respond with ONLY the JSON object, no additional text or explanations.
"""
+
try:
+ # Get analysis plan from AI
response = await self.mydom.callAi([
- {"role": "system", "content": "You are a data analysis expert. Respond with valid JSON only."},
+ {"role": "system", "content": "You are a data analysis expert. Create detailed analysis plans. Respond with valid JSON only."},
{"role": "user", "content": analysisPrompt}
- ], produceUserAnswer = True)
+ ], produceUserAnswer=True)
- # Extract JSON from response
+ # Extract JSON
jsonStart = response.find('{')
jsonEnd = response.rfind('}') + 1
@@ -266,154 +277,249 @@ class AgentAnalyst(AgentBase):
plan = json.loads(response[jsonStart:jsonEnd])
return plan
else:
- # Fallback if JSON not found
+ # Fallback plan
+ logger.warning(f"Not able creating analysis plan, generating fallback plan")
return {
- "analysisType": "general",
- "keyQuestions": ["What insights can be extracted from this data?"],
- "recommendedVisualizations": [],
- "keyInsights": "Analysis plan could not be created",
- "analysisApproach": "General exploratory analysis"
+ "analysisSteps": [
+ {
+ "step": "Basic data analysis",
+ "purpose": "Understand the data structure and content",
+ "datasets": list(datasets.keys()),
+ "techniques": ["summary statistics", "data visualization"],
+ "outputs": ["summary report", "basic visualizations"]
+ }
+ ],
+ "visualizations": [
+ {
+ "type": "basic charts",
+ "purpose": "Show data distribution and relationships",
+ "datasets": list(datasets.keys()),
+ "settings": {}
+ }
+ ],
+ "insights": [
+ {
+ "type": "basic insights",
+ "description": "Key findings from the data",
+ "datasets": list(datasets.keys())
+ }
+ ],
+ "feedback": f"I'll analyze the data and provide insights about {prompt}"
}
except Exception as e:
logger.warning(f"Error creating analysis plan: {str(e)}")
+ # Simple fallback plan
return {
- "analysisType": "general",
- "keyQuestions": ["What insights can be extracted from this data?"],
- "recommendedVisualizations": [],
- "keyInsights": "Analysis plan could not be created",
- "analysisApproach": "General exploratory analysis"
+ "analysisSteps": [
+ {
+ "step": "Basic data analysis",
+ "purpose": "Understand the data structure and content",
+ "datasets": list(datasets.keys()),
+ "techniques": ["summary statistics", "data visualization"],
+ "outputs": ["summary report", "basic visualizations"]
+ }
+ ],
+ "visualizations": [
+ {
+ "type": "basic charts",
+ "purpose": "Show data distribution and relationships",
+ "datasets": list(datasets.keys()),
+ "settings": {}
+ }
+ ],
+ "insights": [
+ {
+ "type": "basic insights",
+ "description": "Key findings from the data",
+ "datasets": list(datasets.keys())
+ }
+ ],
+ "feedback": f"I'll analyze the data and provide insights about {prompt}"
}
async def _createVisualization(self, datasets: Dict, prompt: str, outputLabel: str,
analysisPlan: Dict, description: str) -> Dict:
"""
- Create visualization document using AI guidance.
+ Create a visualization based on the analysis plan.
Args:
datasets: Dictionary of datasets
prompt: Original task prompt
- outputLabel: Output filename
- analysisPlan: Analysis plan from AI
+ outputLabel: Output file label
+ analysisPlan: Analysis plan
description: Output description
Returns:
- Visualization document
+ Document dictionary with visualization
"""
- # Determine format from filename
- formatType = outputLabel.split('.')[-1].lower()
- if formatType not in ['png', 'jpg', 'jpeg', 'svg']:
- formatType = 'png'
-
- # If no datasets available, create error message image
- if not datasets:
- plt.figure(figsize=(10, 6))
- plt.text(0.5, 0.5, "No data available for visualization",
- ha='center', va='center', fontsize=14)
- plt.tight_layout()
- imgData = self._getImageBase64(formatType)
- plt.close()
-
- return {
- "label": outputLabel,
- "content": imgData,
- "metadata": {
- "contentType": f"image/{formatType}"
- }
- }
-
- # Get recommended visualization from plan
- recommendedViz = analysisPlan.get("recommendedVisualizations", [])
-
- # Prepare dataset info for the first dataset if none specified
- if not recommendedViz and datasets:
- name, df = next(iter(datasets.items()))
- recommendedViz = [{
- "type": "auto",
- "dataSource": name,
- "variables": df.columns.tolist()[:5],
- "purpose": "general analysis"
- }]
-
- # Create visualization code prompt
- vizPrompt = f"""
- Generate Python matplotlib/seaborn code to create a visualization for:
-
- TASK: {prompt}
-
- VISUALIZATION REQUIREMENTS:
- - Output format: {formatType}
- - Filename: {outputLabel}
- - Description: {description}
-
- RECOMMENDED VISUALIZATION:
- {json.dumps(recommendedViz, indent=2)}
-
- AVAILABLE DATASETS:
- """
-
- # Add dataset info for recommended sources
- for viz in recommendedViz:
- dataSource = viz.get("dataSource")
- if dataSource in datasets:
- df = datasets[dataSource]
- vizPrompt += f"\nDataset '{dataSource}':\n"
- vizPrompt += f"- Shape: {df.shape}\n"
- vizPrompt += f"- Columns: {df.columns.tolist()}\n"
- vizPrompt += f"- Sample data: {df.head(3).to_dict(orient='records')}\n"
-
- vizPrompt += """
- Generate ONLY Python code that:
- 1. Uses matplotlib and/or seaborn to create a clear visualization
- 2. Sets figure size to (10, 6)
- 3. Includes appropriate titles, labels, and legend
- 4. Uses professional color schemes
- 5. Handles any missing data gracefully
-
- Return ONLY executable Python code, no explanations or markdown.
- """
-
try:
- # Get visualization code from AI
- vizCode = await self.mydom.callAi([
- {"role": "system", "content": "You are a data visualization expert. Provide only executable Python code."},
- {"role": "user", "content": vizPrompt}
- ], produceUserAnswer = True)
+ # Get visualization recommendations
+ vizRecommendations = analysisPlan.get("visualizations", [])
- # Clean code
- vizCode = vizCode.replace("```python", "").replace("```", "").strip()
-
- # Execute visualization code
- plt.figure(figsize=(10, 6))
-
- # Make local variables available to the code
- localVars = {
- "plt": plt,
- "sns": sns,
- "pd": pd,
- "np": __import__('numpy')
- }
-
- # Add datasets to local variables
- for name, df in datasets.items():
- # Create a sanitized variable name
- varName = ''.join(c if c.isalnum() else '_' for c in name)
- localVars[varName] = df
+ if not vizRecommendations:
+ # Generate visualization recommendations if none provided
+ self.mydom.logAdd(analysisPlan.get("workflowId"), "Generating visualization recommendations...", level="info", progress=50)
+ vizPrompt = f"""
+ Based on this data and task, recommend appropriate visualizations.
- # Also add with standard names for simpler code
- if "df" not in localVars:
- localVars["df"] = df
- elif "df2" not in localVars:
- localVars["df2"] = df
+ TASK: {prompt}
+ DESCRIPTION: {description}
+
+ DATASETS:
+ {json.dumps({name: {"shape": df.shape, "columns": df.columns.tolist()}
+ for name, df in datasets.items()}, indent=2)}
+
+ Recommend visualizations in JSON format:
+ {{
+ "visualizations": [
+ {{
+ "type": "chart_type",
+ "dataSource": "dataset_name",
+ "variables": ["col1", "col2"],
+ "purpose": "explanation"
+ }}
+ ]
+ }}
+ """
+
+ response = await self.mydom.callAi([
+ {"role": "system", "content": "You are a data visualization expert. Recommend appropriate visualizations based on the data and task."},
+ {"role": "user", "content": vizPrompt}
+ ])
+
+ # Extract JSON
+ jsonStart = response.find('{')
+ jsonEnd = response.rfind('}') + 1
+
+ if jsonStart >= 0 and jsonEnd > jsonStart:
+ vizData = json.loads(response[jsonStart:jsonEnd])
+ vizRecommendations = vizData.get("visualizations", [])
- # Execute the visualization code
- exec(vizCode, globals(), localVars)
+ # Determine format from filename
+ formatType = outputLabel.split('.')[-1].lower()
+ if formatType not in ['png', 'jpg', 'jpeg', 'svg']:
+ formatType = 'png'
- # Capture the image
- imgData = self._getImageBase64(formatType)
- plt.close()
+ # If no datasets available, create error message image
+ if not datasets:
+ plt.figure(figsize=(10, 6))
+ plt.text(0.5, 0.5, "No data available for visualization",
+ ha='center', va='center', fontsize=14)
+ plt.tight_layout()
+ imgData = self._getImageBase64(formatType)
+ plt.close()
+
+ return {
+ "label": outputLabel,
+ "content": imgData,
+ "metadata": {
+ "contentType": f"image/{formatType}"
+ }
+ }
- return self.formatAgentDocumentOutput(outputLabel, imgData, f"image/{formatType}")
+ # Prepare dataset info for the first dataset if none specified
+ if not vizRecommendations and datasets:
+ name, df = next(iter(datasets.items()))
+ vizRecommendations = [{
+ "type": "auto",
+ "dataSource": name,
+ "variables": df.columns.tolist()[:5],
+ "purpose": "general analysis"
+ }]
+
+ # Create visualization code prompt
+ vizPrompt = f"""
+ Generate Python matplotlib/seaborn code to create a visualization for:
+
+ TASK: {prompt}
+
+ VISUALIZATION REQUIREMENTS:
+ - Output format: {formatType}
+ - Filename: {outputLabel}
+ - Description: {description}
+
+ RECOMMENDED VISUALIZATION:
+ {json.dumps(vizRecommendations, indent=2)}
+
+ AVAILABLE DATASETS:
+ """
+
+ # Add dataset info for recommended sources
+ for viz in vizRecommendations:
+ dataSource = viz.get("dataSource")
+ if dataSource in datasets:
+ df = datasets[dataSource]
+ vizPrompt += f"\nDataset '{dataSource}':\n"
+ vizPrompt += f"- Shape: {df.shape}\n"
+ vizPrompt += f"- Columns: {df.columns.tolist()}\n"
+ vizPrompt += f"- Sample data: {df.head(3).to_dict(orient='records')}\n"
+
+ vizPrompt += """
+ Generate ONLY Python code that:
+ 1. Uses matplotlib and/or seaborn to create a clear visualization
+ 2. Sets figure size to (10, 6)
+ 3. Includes appropriate titles, labels, and legend
+ 4. Uses professional color schemes
+ 5. Handles any missing data gracefully
+
+ Return ONLY executable Python code, no explanations or markdown.
+ """
+
+ try:
+ # Get visualization code from AI
+ vizCode = await self.mydom.callAi([
+ {"role": "system", "content": "You are a data visualization expert. Provide only executable Python code."},
+ {"role": "user", "content": vizPrompt}
+ ], produceUserAnswer = True)
+
+ # Clean code
+ vizCode = vizCode.replace("```python", "").replace("```", "").strip()
+
+ # Execute visualization code
+ plt.figure(figsize=(10, 6))
+
+ # Make local variables available to the code
+ localVars = {
+ "plt": plt,
+ "sns": sns,
+ "pd": pd,
+ "np": __import__('numpy')
+ }
+
+ # Add datasets to local variables
+ for name, df in datasets.items():
+ # Create a sanitized variable name
+ varName = ''.join(c if c.isalnum() else '_' for c in name)
+ localVars[varName] = df
+
+ # Also add with standard names for simpler code
+ if "df" not in localVars:
+ localVars["df"] = df
+ elif "df2" not in localVars:
+ localVars["df2"] = df
+
+ # Execute the visualization code
+ exec(vizCode, globals(), localVars)
+
+ # Capture the image
+ imgData = self._getImageBase64(formatType)
+ plt.close()
+
+ return self.formatAgentDocumentOutput(outputLabel, imgData, f"image/{formatType}")
+
+ except Exception as e:
+ logger.error(f"Error creating visualization: {str(e)}", exc_info=True)
+
+ # Create error message image
+ plt.figure(figsize=(10, 6))
+ plt.text(0.5, 0.5, f"Visualization error: {str(e)}",
+ ha='center', va='center', fontsize=12)
+ plt.tight_layout()
+ imgData = self._getImageBase64(formatType)
+ plt.close()
+
+ return self.formatAgentDocumentOutput(outputLabel, imgData, f"image/{formatType}")
except Exception as e:
logger.error(f"Error creating visualization: {str(e)}", exc_info=True)
diff --git a/modules/agentEmail.py b/modules/agentEmail.py
index 0a6482ab..6686b725 100644
--- a/modules/agentEmail.py
+++ b/modules/agentEmail.py
@@ -81,6 +81,7 @@ class AgentEmail(AgentBase):
# Extract task information
prompt = task.get("prompt", "")
inputDocuments = task.get("inputDocuments", [])
+ outputSpecs = task.get("outputSpecifications", [])
# Check AI service
if not self.mydom:
@@ -128,22 +129,36 @@ class AgentEmail(AgentBase):
# Prepare output documents
documents = []
- # Add HTML preview document
- previewDoc = self.formatAgentDocumentOutput(
- "email_preview.html",
- htmlPreview,
- "text/html"
- )
- documents.append(previewDoc)
-
- # Add email template as JSON for reference
- templateJson = json.dumps(emailTemplate, indent=2)
- templateDoc = self.formatAgentDocumentOutput(
- "email_template.json",
- templateJson,
- "application/json"
- )
- documents.append(templateDoc)
+ # 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:
@@ -230,18 +245,19 @@ class AgentEmail(AgentBase):
# Add document name to contents
documentContents.append(f"\n\n--- {docName} ---\n")
- # Check if document has data to attach
+ # Process document data directly
if doc.get("data"):
- # Add to attachments
+ # Add to attachments with proper metadata
attachments.append({
"name": docName,
- "document": doc
+ "document": {
+ "data": doc["data"],
+ "mimeType": doc.get("mimeType", "application/octet-stream"),
+ "base64Encoded": doc.get("base64Encoded", False)
+ }
})
-
- # Add document name to contents
documentContents.append(f"Document attached: {docName}")
else:
- # If no data, just add the name
documentContents.append(f"Document referenced: {docName}")
return "\n".join(documentContents), attachments
@@ -282,7 +298,7 @@ class AgentEmail(AgentBase):
try:
response = await self.mydom.callAi([
- {"role": "system", "content": "You are an email template specialist. Respond with valid JSON only."},
+ {"role": "system", "content": "You are an email template specialist. Create professional emails. Respond with valid JSON only."},
{"role": "user", "content": emailPrompt}
], produceUserAnswer=True)
@@ -294,7 +310,8 @@ class AgentEmail(AgentBase):
template = json.loads(response[jsonStart:jsonEnd])
return template
else:
- # Fallback if JSON not found
+ # Fallback plan
+ logger.warning(f"Not able creating email template, generating fallback plan")
return {
"recipient": "recipient@example.com",
"subject": "Information Regarding Your Request",
@@ -471,8 +488,8 @@ class AgentEmail(AgentBase):
def _createGraphDraftEmail(self, access_token, recipient, subject, body, attachments=None):
"""
- Create a draft email using Microsoft Graph API with fixed attachment handling.
- Uses the document data directly for attachments.
+ Create a draft email using Microsoft Graph API.
+ Treats all files as binary attachments without content analysis.
Args:
access_token: Microsoft Graph access token
@@ -521,87 +538,69 @@ class AgentEmail(AgentBase):
logger.warning(f"No data found for attachment: {file_name}")
continue
- # Get content type and base64 flag
- content_type = doc.get('contentType', 'application/octet-stream')
+ # Get content type from document metadata
+ mime_type = doc.get('mimeType', 'application/octet-stream')
is_base64 = doc.get('base64Encoded', False)
- # Handle base64 encoding if needed
- if not is_base64:
- logger.info(f"Base64 encoding content for {file_name}")
- if isinstance(file_content, str):
- try:
- # Check if already valid base64
- base64.b64decode(file_content)
- logger.info("Content appears to be valid base64 already")
- except:
- # Not valid base64, encode it
- logger.info("Encoding string content to base64")
- file_content = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
- elif isinstance(file_content, bytes):
- logger.info("Encoding bytes content to base64")
- file_content = base64.b64encode(file_content).decode('utf-8')
-
- # Calculate actual size from base64 content
+ # Handle content encoding
try:
- decoded_size = len(base64.b64decode(file_content))
+ if is_base64:
+ # Content is already base64 encoded
+ content_bytes = file_content
+ else:
+ # Content needs to be base64 encoded
+ if isinstance(file_content, str):
+ # For text files, encode the string to bytes first
+ content_bytes = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
+ elif isinstance(file_content, bytes):
+ # For binary files, encode directly
+ content_bytes = base64.b64encode(file_content).decode('utf-8')
+ else:
+ logger.warning(f"Unexpected content type for {file_name}")
+ continue
+
+ # Calculate size from decoded content
+ decoded_size = len(base64.b64decode(content_bytes))
+
+ # Add attachment to email data
+ logger.info(f"Adding attachment: {file_name} ({mime_type}, size: {decoded_size} bytes)")
+ attachment_data = {
+ '@odata.type': '#microsoft.graph.fileAttachment',
+ 'name': file_name,
+ 'contentType': mime_type,
+ 'contentBytes': content_bytes,
+ 'isInline': False,
+ 'size': decoded_size
+ }
+ email_data['attachments'].append(attachment_data)
+ logger.info(f"Successfully added attachment: {file_name}")
+
except Exception as e:
- logger.error(f"Error calculating size for {file_name}: {str(e)}")
- decoded_size = 0
-
- # Add attachment to email data
- logger.info(f"Adding attachment: {file_name} ({content_type}, size: {decoded_size} bytes)")
- attachment_data = {
- '@odata.type': '#microsoft.graph.fileAttachment',
- 'name': file_name,
- 'contentType': content_type,
- 'contentBytes': file_content,
- 'isInline': False,
- 'size': decoded_size
- }
- email_data['attachments'].append(attachment_data)
- logger.info(f"Successfully added attachment: {file_name}")
+ logger.error(f"Error processing attachment {file_name}: {str(e)}")
+ continue
# Try to create draft using drafts folder endpoint
try:
- logger.info("Attempting to create draft email using drafts folder endpoint")
+ logger.info("Attempting to create draft email using messages endpoint")
logger.info(f"Email data structure: subject={subject}, recipient={recipient}, " +
f"has_attachments={bool(email_data.get('attachments'))}, " +
f"attachment_count={len(email_data.get('attachments', []))}")
- # Log the full email data structure for debugging
- logger.debug(f"Full email data structure: {json.dumps(email_data, indent=2)}")
-
- # First create the draft message
+ # Create the draft message
response = requests.post(
- 'https://graph.microsoft.com/v1.0/me/mailFolders/drafts/messages',
+ 'https://graph.microsoft.com/v1.0/me/messages',
headers=headers,
json=email_data
)
if response.status_code >= 200 and response.status_code < 300:
- logger.info("Successfully created draft email using drafts folder endpoint")
+ logger.info("Successfully created draft email using messages endpoint")
return response.json()
else:
- logger.error(f"Drafts folder method failed: {response.status_code} - {response.text}")
+ logger.error(f"Messages endpoint method failed: {response.status_code} - {response.text}")
logger.error(f"Request headers: {headers}")
logger.error(f"Request body: {json.dumps(email_data, indent=2)}")
-
- # Try fallback method with messages endpoint
- logger.info("Trying fallback with messages endpoint")
- response = requests.post(
- 'https://graph.microsoft.com/v1.0/me/messages',
- headers=headers,
- json=email_data
- )
-
- if response.status_code >= 200 and response.status_code < 300:
- logger.info("Successfully created draft email using messages endpoint")
- return response.json()
- else:
- logger.error(f"Messages endpoint method also failed: {response.status_code} - {response.text}")
- logger.error(f"Request headers: {headers}")
- logger.error(f"Request body: {json.dumps(email_data, indent=2)}")
- return None
+ return None
except Exception as e:
logger.error(f"Exception creating draft email: {str(e)}", exc_info=True)
diff --git a/modules/agentWebcrawler.py b/modules/agentWebcrawler.py
index 7f5cad09..7dd87825 100644
--- a/modules/agentWebcrawler.py
+++ b/modules/agentWebcrawler.py
@@ -77,6 +77,7 @@ class AgentWebcrawler(AgentBase):
}
# Create research plan
+ self.mydom.logAdd(task["workflowId"], "Creating research plan...", level="info", progress=35)
researchPlan = await self._createResearchPlan(prompt)
# Check if this is truly a web research task
@@ -87,9 +88,11 @@ class AgentWebcrawler(AgentBase):
}
# Gather raw material through web research
+ self.mydom.logAdd(task["workflowId"], "Gathering research material...", level="info", progress=45)
rawResults = await self._gatherResearchMaterial(researchPlan)
# Format results into requested output documents
+ self.mydom.logAdd(task["workflowId"], "Creating output documents...", level="info", progress=55)
documents = await self._createOutputDocuments(
prompt,
rawResults,
@@ -142,9 +145,9 @@ class AgentWebcrawler(AgentBase):
try:
# Get research plan from AI
response = await self.mydom.callAi([
- {"role": "system", "content": "You are a web research planning expert. Create precise research plans in JSON format only."},
+ {"role": "system", "content": "You are a web research planning expert. Create precise research plans. Respond with valid JSON only."},
{"role": "user", "content": researchPrompt}
- ])
+ ], produceUserAnswer=True)
# Extract JSON
jsonStart = response.find('{')
@@ -202,7 +205,9 @@ class AgentWebcrawler(AgentBase):
# Process direct URLs
directUrls = researchPlan.get("directUrls", [])[:self.maxUrl]
- for url in directUrls:
+ for i, url in enumerate(directUrls):
+ progress = 45 + int((i / len(directUrls)) * 5) # Progress from 45% to 50%
+ self.mydom.logAdd(researchPlan.get("workflowId"), f"Processing direct URL {i+1}/{len(directUrls)}...", level="info", progress=progress)
logger.info(f"Processing direct URL: {url}")
try:
# Fetch and extract content
@@ -226,7 +231,9 @@ class AgentWebcrawler(AgentBase):
# Process search terms
searchTerms = researchPlan.get("searchTerms", [])[:self.maxSearchTerms]
- for term in searchTerms:
+ for i, term in enumerate(searchTerms):
+ progress = 50 + int((i / len(searchTerms)) * 5) # Progress from 50% to 55%
+ self.mydom.logAdd(researchPlan.get("workflowId"), f"Searching term {i+1}/{len(searchTerms)}...", level="info", progress=progress)
logger.info(f"Searching for: {term}")
try:
# Perform search
@@ -302,19 +309,15 @@ class AgentWebcrawler(AgentBase):
Only include information actually found in the content. No fabrications or assumptions.
"""
- if self.mydom:
- summary = await self.mydom.callAi([
- {"role": "system", "content": "You summarize web content accurately and concisely, focusing only on what is actually in the content."},
- {"role": "user", "content": summaryPrompt}
- ])
-
- # Store the summary
- result["summary"] = summary
- else:
- # Fallback if no AI service
- logger.warning(f"Not able to summarize result, using fallback plan.")
- result["summary"] = f"Content from {result['url']} ({len(content)} characters)"
-
+ # Get summary from AI
+ summary = await self.mydom.callAi([
+ {"role": "system", "content": "You are a web content summarization expert. Create concise summaries."},
+ {"role": "user", "content": summaryPrompt}
+ ], produceUserAnswer=True)
+
+ # 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)}"
diff --git a/modules/documentProcessor.py b/modules/documentProcessor.py
index d3b637e1..3e082578 100644
--- a/modules/documentProcessor.py
+++ b/modules/documentProcessor.py
@@ -38,8 +38,50 @@ def getDocumentContents(fileMetadata: Dict[str, Any], fileContent: bytes) -> Lis
# Extract content based on MIME type
contents = []
+ # Try to detect actual file type from content for unknown MIME types
+ if mimeType == "application/octet-stream":
+ # Check file extension first
+ ext = os.path.splitext(fileName)[1].lower()
+ if ext:
+ # Map common extensions to MIME types
+ ext_to_mime = {
+ '.txt': 'text/plain',
+ '.md': 'text/markdown',
+ '.csv': 'text/csv',
+ '.json': 'application/json',
+ '.xml': 'application/xml',
+ '.js': 'application/javascript',
+ '.py': 'application/x-python',
+ '.svg': 'image/svg+xml',
+ '.jpg': 'image/jpeg',
+ '.jpeg': 'image/jpeg',
+ '.png': 'image/png',
+ '.gif': 'image/gif',
+ '.pdf': 'application/pdf',
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ '.doc': 'application/msword',
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ '.xls': 'application/vnd.ms-excel',
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ '.ppt': 'application/vnd.ms-powerpoint'
+ }
+ if ext in ext_to_mime:
+ mimeType = ext_to_mime[ext]
+ logger.info(f"Detected MIME type {mimeType} from extension {ext}")
+ else:
+ logger.warning(f"Unknown file extension {ext} for file {fileName}")
+
+ # Try to detect if it's text content
+ try:
+ text_content = fileContent.decode('utf-8')
+ logger.info(f"Successfully decoded file {fileName} as text")
+ contents.extend(extractTextContent(fileName, fileContent, "text/plain"))
+ except UnicodeDecodeError:
+ logger.info(f"File {fileName} is not text, treating as binary")
+ contents.extend(extractBinaryContent(fileName, fileContent, mimeType))
+
# Text-based formats (excluding CSV which has its own handler)
- if mimeType == "text/csv":
+ elif mimeType == "text/csv":
contents.extend(extractCsvContent(fileName, fileContent))
# Then handle other text-based formats
@@ -86,6 +128,7 @@ def getDocumentContents(fileMetadata: Dict[str, Any], fileContent: bytes) -> Lis
# Binary data as fallback for unknown formats
else:
+ logger.warning(f"Unknown MIME type {mimeType} for file {fileName}, treating as binary")
contents.extend(extractBinaryContent(fileName, fileContent, mimeType))
# Fallback when no content could be extracted
@@ -99,7 +142,7 @@ def getDocumentContents(fileMetadata: Dict[str, Any], fileContent: bytes) -> Lis
"sequenceNr": 1,
"name": '1_undefined',
"ext": os.path.splitext(fileName)[1][1:] if os.path.splitext(fileName)[1] else "bin",
- "contentType": mimeType,
+ "mimeType": mimeType,
"data": encoded_data,
"base64Encoded": True,
"metadata": {
@@ -130,13 +173,13 @@ def getDocumentContents(fileMetadata: Dict[str, Any], fileContent: bytes) -> Lis
return contents
except Exception as e:
- logger.error(f"Error during content extraction: {str(e)}")
+ logger.error(f"Error during content extraction for file {fileMetadata.get('name', 'unknown')}: {str(e)}", exc_info=True)
# Fallback on error - return original data
return [{
"sequenceNr": 1,
"name": fileMetadata.get("name", "unknown"),
"ext": os.path.splitext(fileMetadata.get("name", ""))[1][1:] if os.path.splitext(fileMetadata.get("name", ""))[1] else "bin",
- "contentType": fileMetadata.get("mimeType", "application/octet-stream"),
+ "mimeType": fileMetadata.get("mimeType", "application/octet-stream"),
"data": base64.b64encode(fileContent).decode('utf-8'),
"base64Encoded": True,
"metadata": {
@@ -206,7 +249,7 @@ def extractTextContent(fileName: str, fileContent: bytes, mimeType: str) -> List
"sequenceNr": 1,
"name": "1_text", # Simplified naming
"ext": fileExtension,
- "contentType": "text/plain",
+ "mimeType": "text/plain",
"data": textContent,
"base64Encoded": False,
"metadata": {
@@ -225,7 +268,7 @@ def extractTextContent(fileName: str, fileContent: bytes, mimeType: str) -> List
"sequenceNr": 1,
"name": "1_text", # Simplified naming
"ext": fileExtension,
- "contentType": "text/plain",
+ "mimeType": "text/plain",
"data": textContent,
"base64Encoded": False,
"metadata": {
@@ -242,7 +285,7 @@ def extractTextContent(fileName: str, fileContent: bytes, mimeType: str) -> List
"sequenceNr": 1,
"name": "1_binary", # Simplified naming
"ext": fileExtension,
- "contentType": mimeType,
+ "mimeType": mimeType,
"data": base64.b64encode(fileContent).decode('utf-8'),
"base64Encoded": True,
"metadata": {
@@ -256,7 +299,7 @@ def extractTextContent(fileName: str, fileContent: bytes, mimeType: str) -> List
"sequenceNr": 1,
"name": "1_binary", # Simplified naming
"ext": fileExtension,
- "contentType": mimeType,
+ "mimeType": mimeType,
"data": base64.b64encode(fileContent).decode('utf-8'),
"base64Encoded": True,
"metadata": {
@@ -282,7 +325,7 @@ def extractCsvContent(fileName: str, fileContent: bytes) -> List[Dict[str, Any]]
"sequenceNr": 1,
"name": "1_csv", # Simplified naming
"ext": "csv",
- "contentType": "text/csv",
+ "mimeType": "text/csv",
"data": csvContent,
"base64Encoded": False,
"metadata": {
@@ -302,7 +345,7 @@ def extractCsvContent(fileName: str, fileContent: bytes) -> List[Dict[str, Any]]
"sequenceNr": 1,
"name": "1_csv", # Simplified naming
"ext": "csv",
- "contentType": "text/csv",
+ "mimeType": "text/csv",
"data": csvContent,
"base64Encoded": False,
"metadata": {
@@ -319,7 +362,7 @@ def extractCsvContent(fileName: str, fileContent: bytes) -> List[Dict[str, Any]]
"sequenceNr": 1,
"name": "1_binary", # Simplified naming
"ext": "csv",
- "contentType": "text/csv",
+ "mimeType": "text/csv",
"data": base64.b64encode(fileContent).decode('utf-8'),
"base64Encoded": True,
"metadata": {
@@ -332,7 +375,7 @@ def extractCsvContent(fileName: str, fileContent: bytes) -> List[Dict[str, Any]]
"sequenceNr": 1,
"name": "1_binary", # Simplified naming
"ext": "csv",
- "contentType": "text/csv",
+ "mimeType": "text/csv",
"data": base64.b64encode(fileContent).decode('utf-8'),
"base64Encoded": True,
"metadata": {
@@ -364,7 +407,7 @@ def extractSvgContent(fileName: str, fileContent: bytes) -> List[Dict[str, Any]]
"sequenceNr": 1,
"name": "1_svg", # Simplified naming
"ext": "svg",
- "contentType": "image/svg+xml",
+ "mimeType": "image/svg+xml",
"data": svgText,
"base64Encoded": False,
"metadata": {
@@ -380,7 +423,7 @@ def extractSvgContent(fileName: str, fileContent: bytes) -> List[Dict[str, Any]]
"sequenceNr": 1,
"name": "1_text",
"ext": "svg",
- "contentType": "text/plain",
+ "mimeType": "text/plain",
"data": svgText,
"base64Encoded": False,
"metadata": {
@@ -401,7 +444,7 @@ def extractSvgContent(fileName: str, fileContent: bytes) -> List[Dict[str, Any]]
"sequenceNr": 1,
"name": "1_svg", # Simplified naming
"ext": "svg",
- "contentType": "image/svg+xml",
+ "mimeType": "image/svg+xml",
"data": svgText,
"base64Encoded": False,
"metadata": {
@@ -422,7 +465,7 @@ def extractSvgContent(fileName: str, fileContent: bytes) -> List[Dict[str, Any]]
"sequenceNr": 1,
"name": "1_binary", # Simplified naming
"ext": "svg",
- "contentType": "image/svg+xml",
+ "mimeType": "image/svg+xml",
"data": base64.b64encode(fileContent).decode('utf-8'),
"base64Encoded": True,
"metadata": {
@@ -438,7 +481,7 @@ def extractSvgContent(fileName: str, fileContent: bytes) -> List[Dict[str, Any]]
"sequenceNr": 1,
"name": "1_binary", # Simplified naming
"ext": "svg",
- "contentType": "image/svg+xml",
+ "mimeType": "image/svg+xml",
"data": base64.b64encode(fileContent).decode('utf-8'),
"base64Encoded": True,
"metadata": {
@@ -519,7 +562,7 @@ def extractImageContent(fileName: str, fileContent: bytes, mimeType: str) -> Lis
"sequenceNr": 1,
"name": "1_image", # Simplified naming
"ext": fileExtension,
- "contentType": mimeType,
+ "mimeType": mimeType,
"data": encoded_data,
"base64Encoded": True,
"metadata": imageMetadata
@@ -531,7 +574,7 @@ def extractImageContent(fileName: str, fileContent: bytes, mimeType: str) -> Lis
"sequenceNr": 2,
"name": "2_text_image_info", # Simplified naming with label
"ext": "txt",
- "contentType": "text/plain",
+ "mimeType": "text/plain",
"data": imageDescription,
"base64Encoded": False,
"metadata": {
@@ -566,7 +609,7 @@ def extractPdfContent(fileName: str, fileContent: bytes) -> List[Dict[str, Any]]
"sequenceNr": 1,
"name": "1_pdf", # Simplified naming
"ext": "pdf",
- "contentType": "application/pdf",
+ "mimeType": "application/pdf",
"data": base64.b64encode(fileContent).decode('utf-8'),
"base64Encoded": True,
"metadata": {
@@ -604,7 +647,7 @@ def extractPdfContent(fileName: str, fileContent: bytes) -> List[Dict[str, Any]]
"sequenceNr": len(contents) + 1,
"name": f"{len(contents) + 1}_text", # Simplified naming
"ext": "txt",
- "contentType": "text/plain",
+ "mimeType": "text/plain",
"data": extractedText,
"base64Encoded": False,
"metadata": {
@@ -639,7 +682,7 @@ def extractPdfContent(fileName: str, fileContent: bytes) -> List[Dict[str, Any]]
"sequenceNr": len(contents) + 1,
"name": f"{len(contents) + 1}_image_page{pageNum+1}_{imgIndex+1}", # Simplified naming with label
"ext": imageExt,
- "contentType": f"image/{imageExt}",
+ "mimeType": f"image/{imageExt}",
"data": base64.b64encode(imageBytes).decode('utf-8'),
"base64Encoded": True,
"metadata": {
@@ -667,7 +710,7 @@ def extractPdfContent(fileName: str, fileContent: bytes) -> List[Dict[str, Any]]
"sequenceNr": 1,
"name": "1_pdf", # Simplified naming
"ext": "pdf",
- "contentType": "application/pdf",
+ "mimeType": "application/pdf",
"data": base64.b64encode(fileContent).decode('utf-8'),
"base64Encoded": True,
"metadata": {
@@ -706,7 +749,7 @@ def extractWordContent(fileName: str, fileContent: bytes, mimeType: str) -> List
"sequenceNr": 1,
"name": "1_word", # Simplified naming
"ext": fileExtension,
- "contentType": mimeType,
+ "mimeType": mimeType,
"data": base64.b64encode(fileContent).decode('utf-8'),
"base64Encoded": True,
"metadata": {
@@ -743,7 +786,7 @@ def extractWordContent(fileName: str, fileContent: bytes, mimeType: str) -> List
"sequenceNr": 1,
"name": "1_text", # Simplified naming
"ext": "txt",
- "contentType": "text/plain",
+ "mimeType": "text/plain",
"data": extractedText,
"base64Encoded": False,
"metadata": {
@@ -765,7 +808,7 @@ def extractWordContent(fileName: str, fileContent: bytes, mimeType: str) -> List
"sequenceNr": 1,
"name": "1_word", # Simplified naming
"ext": fileExtension,
- "contentType": mimeType,
+ "mimeType": mimeType,
"data": base64.b64encode(fileContent).decode('utf-8'),
"base64Encoded": True,
"metadata": {
@@ -804,7 +847,7 @@ def extractExcelContent(fileName: str, fileContent: bytes, mimeType: str) -> Lis
"sequenceNr": 1,
"name": "1_excel", # Simplified naming
"ext": fileExtension,
- "contentType": mimeType,
+ "mimeType": mimeType,
"data": base64.b64encode(fileContent).decode('utf-8'),
"base64Encoded": True,
"metadata": {
@@ -845,7 +888,7 @@ def extractExcelContent(fileName: str, fileContent: bytes, mimeType: str) -> Lis
"sequenceNr": len(contents) + 1,
"name": f"{len(contents) + 1}_csv_{sheetSafeName}", # Simplified naming with sheet label
"ext": "csv",
- "contentType": "text/csv",
+ "mimeType": "text/csv",
"data": csvContent,
"base64Encoded": False,
"metadata": {
@@ -867,7 +910,7 @@ def extractExcelContent(fileName: str, fileContent: bytes, mimeType: str) -> Lis
"sequenceNr": 1,
"name": "1_excel", # Simplified naming
"ext": fileExtension,
- "contentType": mimeType,
+ "mimeType": mimeType,
"data": base64.b64encode(fileContent).decode('utf-8'),
"base64Encoded": True,
"metadata": {
@@ -897,7 +940,7 @@ def extractPowerpointContent(fileName: str, fileContent: bytes, mimeType: str) -
"sequenceNr": 1,
"name": "1_powerpoint", # Simplified naming
"ext": fileExtension,
- "contentType": mimeType,
+ "mimeType": mimeType,
"data": base64.b64encode(fileContent).decode('utf-8'),
"base64Encoded": True,
"metadata": {
@@ -923,11 +966,165 @@ def extractBinaryContent(fileName: str, fileContent: bytes, mimeType: str) -> Li
"sequenceNr": 1,
"name": "1_binary", # Simplified naming
"ext": fileExtension,
- "contentType": mimeType,
+ "mimeType": mimeType,
"data": base64.b64encode(fileContent).decode('utf-8'),
"base64Encoded": True,
"metadata": {
"isText": False,
"format": "binary"
}
- }]
\ No newline at end of file
+ }]
+
+def processFile(self, fileContent: bytes, fileName: str, fileMetadata: Dict[str, Any] = None) -> List[Dict[str, Any]]:
+ """
+ Process a file and return its contents as a list of documents.
+
+ Args:
+ fileContent: Binary content of the file
+ fileName: Name of the file
+ fileMetadata: Optional metadata about the file
+
+ Returns:
+ List of document dictionaries
+ """
+ try:
+ # Get file extension and MIME type
+ fileExtension = os.path.splitext(fileName)[1].lower()[1:]
+ mimeType = fileMetadata.get("mimeType", self.mydom.getMimeType(fileName)) if fileMetadata else self.mydom.getMimeType(fileName)
+
+ # Process based on file type
+ if mimeType.startswith("image/"):
+ return self._processImageFile(fileContent, fileName, fileExtension, mimeType, fileMetadata)
+ elif mimeType == "application/pdf":
+ return self._processPdfFile(fileContent, fileName, fileMetadata)
+ elif mimeType == "text/csv":
+ return self._processCsvFile(fileContent, fileName, fileMetadata)
+ elif mimeType == "text/plain":
+ return self._processTextFile(fileContent, fileName, fileMetadata)
+ else:
+ # Default binary file handling
+ return [{
+ "name": fileName,
+ "ext": fileExtension,
+ "mimeType": mimeType,
+ "data": base64.b64encode(fileContent).decode('utf-8'),
+ "base64Encoded": True,
+ "metadata": {
+ "isText": False
+ }
+ }]
+
+ except Exception as e:
+ logger.error(f"Error processing file {fileName}: {str(e)}")
+ raise FileProcessingError(f"Error processing file: {str(e)}")
+
+ def _processImageFile(self, fileContent: bytes, fileName: str, fileExtension: str, mimeType: str, fileMetadata: Dict[str, Any] = None) -> List[Dict[str, Any]]:
+ """Process an image file."""
+ try:
+ # Create image document
+ imageDoc = {
+ "name": fileName,
+ "ext": fileExtension,
+ "mimeType": mimeType,
+ "data": base64.b64encode(fileContent).decode('utf-8'),
+ "base64Encoded": True,
+ "metadata": {
+ "isText": False,
+ "isImage": True,
+ "format": fileExtension
+ }
+ }
+
+ # Add image description if available
+ if fileMetadata and "description" in fileMetadata:
+ imageDoc["metadata"]["description"] = fileMetadata["description"]
+
+ return [imageDoc]
+
+ except Exception as e:
+ logger.error(f"Error processing image file {fileName}: {str(e)}")
+ raise FileProcessingError(f"Error processing image file: {str(e)}")
+
+ def _processPdfFile(self, fileContent: bytes, fileName: str, fileMetadata: Dict[str, Any] = None) -> List[Dict[str, Any]]:
+ """Process a PDF file."""
+ try:
+ # Create PDF document
+ pdfDoc = {
+ "name": fileName,
+ "ext": "pdf",
+ "mimeType": "application/pdf",
+ "data": base64.b64encode(fileContent).decode('utf-8'),
+ "base64Encoded": True,
+ "metadata": {
+ "isText": False,
+ "isPdf": True
+ }
+ }
+
+ return [pdfDoc]
+
+ except Exception as e:
+ logger.error(f"Error processing PDF file {fileName}: {str(e)}")
+ raise FileProcessingError(f"Error processing PDF file: {str(e)}")
+
+ def _processCsvFile(self, fileContent: bytes, fileName: str, fileMetadata: Dict[str, Any] = None) -> List[Dict[str, Any]]:
+ """Process a CSV file."""
+ try:
+ # Try to decode as text first
+ try:
+ csvContent = fileContent.decode('utf-8')
+ base64Encoded = False
+ except UnicodeDecodeError:
+ # If not valid UTF-8, encode as base64
+ csvContent = base64.b64encode(fileContent).decode('utf-8')
+ base64Encoded = True
+
+ # Create CSV document
+ csvDoc = {
+ "name": fileName,
+ "ext": "csv",
+ "mimeType": "text/csv",
+ "data": csvContent,
+ "base64Encoded": base64Encoded,
+ "metadata": {
+ "isText": True,
+ "isCsv": True,
+ "format": "csv"
+ }
+ }
+
+ return [csvDoc]
+
+ except Exception as e:
+ logger.error(f"Error processing CSV file {fileName}: {str(e)}")
+ raise FileProcessingError(f"Error processing CSV file: {str(e)}")
+
+ def _processTextFile(self, fileContent: bytes, fileName: str, fileMetadata: Dict[str, Any] = None) -> List[Dict[str, Any]]:
+ """Process a text file."""
+ try:
+ # Try to decode as text
+ try:
+ textContent = fileContent.decode('utf-8')
+ base64Encoded = False
+ except UnicodeDecodeError:
+ # If not valid UTF-8, encode as base64
+ textContent = base64.b64encode(fileContent).decode('utf-8')
+ base64Encoded = True
+
+ # Create text document
+ textDoc = {
+ "name": fileName,
+ "ext": "txt",
+ "mimeType": "text/plain",
+ "data": textContent,
+ "base64Encoded": base64Encoded,
+ "metadata": {
+ "isText": True
+ }
+ }
+
+ return [textDoc]
+
+ except Exception as e:
+ logger.error(f"Error processing text file {fileName}: {str(e)}")
+ raise FileProcessingError(f"Error processing text file: {str(e)}")
\ No newline at end of file
diff --git a/modules/lucydomInterface.py b/modules/lucydomInterface.py
index a2106d6d..b09629d5 100644
--- a/modules/lucydomInterface.py
+++ b/modules/lucydomInterface.py
@@ -358,11 +358,14 @@ class LucyDOMInterface:
return hashlib.sha256(fileContent).hexdigest()
def checkForDuplicateFile(self, fileHash: str) -> Optional[Dict[str, Any]]:
- """Checks if a file with the same hash already exists."""
- files = self.db.getRecordset("files", recordFilter={"fileHash": fileHash})
- filteredFiles = self._uam("files", files)
- if filteredFiles:
- return filteredFiles[0]
+ """Checks if a file with the same hash already exists for the current user and mandate."""
+ files = self.db.getRecordset("files", recordFilter={
+ "fileHash": fileHash,
+ "mandateId": self.mandateId,
+ "userId": self.userId
+ })
+ if files:
+ return files[0]
return None
def getMimeType(self, filename: str) -> str:
@@ -670,7 +673,7 @@ class LucyDOMInterface:
fileHash = self.calculateFileHash(fileContent)
logger.debug(f"Calculated file hash: {fileHash}")
- # Check for duplicate
+ # Check for duplicate within same user/mandate
existingFile = self.checkForDuplicateFile(fileHash)
if existingFile:
logger.info(f"Duplicate found for {fileName}: {existingFile['id']}")
@@ -692,9 +695,6 @@ class LucyDOMInterface:
# Save binary data
logger.info(f"Saving file content to database for file: {fileName}")
self.createFileData(dbFile["id"], fileContent)
-
- # Debug: Export file to static folder
- self._exportFileToStatic(fileContent, dbFile["id"], fileName)
logger.info(f"File upload process completed for: {fileName}")
return dbFile
@@ -731,12 +731,6 @@ class LucyDOMInterface:
logger.error(f"Error downloading file {fileId}: {str(e)}")
raise FileError(f"Error downloading file: {str(e)}")
- def _exportFileToStatic(self, fileContent: bytes, fileId: int, fileName: str):
- """Debug helper to export files to static folder."""
- debugFilename = f"{fileId}_{fileName}"
- with open(f"./static/{debugFilename}", 'wb') as f:
- f.write(fileContent)
-
# Workflow methods
def getAllWorkflows(self) -> List[Dict[str, Any]]:
diff --git a/modules/lucydomModel.py b/modules/lucydomModel.py
index c14ab9f3..782d0b0e 100644
--- a/modules/lucydomModel.py
+++ b/modules/lucydomModel.py
@@ -110,7 +110,7 @@ class DocumentContent(BaseModel):
sequenceNr: int = Field(1, description="Sequence number of the content in the source document")
name: str = Field(description="Designation")
ext: str = Field(description="Content extension for export: txt, csv, json, jpg, png")
- contentType: str = Field(description="MIME type")
+ mimeType: str = Field(description="MIME type")
summary: str = Field(description="Summary of the file content")
data: str = Field(description="Actual content, text or base64 encoded based on base64Encoded flag")
base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
@@ -122,6 +122,7 @@ class Document(BaseModel):
name: str = Field(description="Name of the data object")
ext: str = Field(description="Extension of the data object")
fileId: int = Field(description="ID of the referenced file in the database")
+ mimeType: str = Field(description="MIME type")
data: str = Field(description="Content of the data as text or base64 encoded based on base64Encoded flag")
base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
contents: List[DocumentContent] = Field(description="Document contents")
diff --git a/modules/workflowAgentsRegistry.py b/modules/workflowAgentsRegistry.py
index 25d8d2ff..0847442b 100644
--- a/modules/workflowAgentsRegistry.py
+++ b/modules/workflowAgentsRegistry.py
@@ -85,51 +85,45 @@ class AgentBase:
"""Wrapper for the utility function"""
return isTextMimeType(mimeType)
- def formatAgentDocumentOutput(self, label: str, content: Any, contentType: str = None) -> Dict[str, Any]:
+ def formatAgentDocumentOutput(self, label: str, content: Any, mimeType: str = None) -> Dict[str, Any]:
"""
- Helper method to properly format a document output with base64Encoded flag and metadata.
+ Format agent output as a document.
Args:
- label: Name of the document
+ label: Label for the document
content: Content of the document
- contentType: Optional content type for the document
-
- Returns:
- Properly formatted document dictionary
+ mimeType: Optional MIME type for the document
"""
- import base64
-
- # Determine if content should be base64 encoded
- should_base64_encode = self.determineBase64EncodingFlag(label, content)
-
- # Process content based on type and encoding flag
- formatted_content = content
-
- if should_base64_encode:
- if isinstance(content, bytes):
- # Convert binary to base64
- formatted_content = base64.b64encode(content).decode('utf-8')
- elif isinstance(content, str):
- try:
- # Check if it's already base64 encoded
- base64.b64decode(content)
- # If we get here, it appears to be valid base64
- formatted_content = content
- except:
- # Not valid base64, so encode it
- formatted_content = base64.b64encode(content.encode('utf-8')).decode('utf-8')
-
- # Create document with metadata
+ # Create document structure
doc = {
- "label": label,
- "content": formatted_content,
- "base64Encoded": should_base64_encode,
- "metadata": {}
+ "id": str(uuid.uuid4()),
+ "name": label,
+ "ext": "txt", # Default extension
+ "data": content,
+ "base64Encoded": False,
+ "metadata": {
+ "isText": True
+ }
}
- # Add content type if provided
- if contentType:
- doc["metadata"]["contentType"] = contentType
+ # Set MIME type if provided
+ if mimeType:
+ doc["mimeType"] = mimeType
+ # Update extension based on MIME type
+ if mimeType == "text/markdown":
+ doc["ext"] = "md"
+ elif mimeType == "text/html":
+ doc["ext"] = "html"
+ elif mimeType == "text/csv":
+ doc["ext"] = "csv"
+ elif mimeType == "application/json":
+ doc["ext"] = "json"
+ elif mimeType.startswith("image/"):
+ doc["ext"] = mimeType.split("/")[1]
+ doc["metadata"]["isText"] = False
+ elif mimeType == "application/pdf":
+ doc["ext"] = "pdf"
+ doc["metadata"]["isText"] = False
return doc
diff --git a/modules/workflowManager.py b/modules/workflowManager.py
index 5d89d311..c4a6b520 100644
--- a/modules/workflowManager.py
+++ b/modules/workflowManager.py
@@ -10,7 +10,7 @@ import json
import re
import uuid
import base64
-from datetime import datetime
+from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional, Union, Tuple
from modules.mimeUtils import isTextMimeType, determineContentEncoding
@@ -382,6 +382,7 @@ Please analyze the request and create:
3. Do not define document inputs that don't exist or haven't been generated beforehand.
4. Create a logical sequence - earlier agents can create documents that are later used as inputs.
5. If the user has provided documents but hasn't clearly stated what they want, try to act according to the context.
+6. ALL documents provided by the user (where fileSource is "user") MUST be included in the work plan, even if they don't have content summaries or if content extraction failed.
Your answer must be strictly in the JSON_OUTPUT format, with no additions before or after the JSON object.
@@ -415,6 +416,7 @@ JSON_OUTPUT = {{
## RULES for inputDocuments:
1. The user request refers to documents where "fileSource" in available documents is "user". Those documents are in the focus for input
2. In case of redundant label in available documents, use document with highest sequenceNr if not specified differently
+3. ALL documents provided by the user MUST be included in the work plan, even if they don't have content summaries or if content extraction failed
## STRICT RULES FOR document "label":
1. Every document label MUST include a proper file extension that matches the content type.
@@ -789,8 +791,13 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
"fileId": fileId,
"name": os.path.splitext(fileNameExt)[0] if os.path.splitext(fileNameExt)[0] else "noname",
"ext": os.path.splitext(fileNameExt)[1][1:] if os.path.splitext(fileNameExt)[1] else "bin",
+ "mimeType": mimeType,
"data": encodedData,
"base64Encoded": base64Encoded,
+ "metadata": {
+ "isText": isTextFormat,
+ "base64Encoded": base64Encoded # For backward compatibility
+ },
"contents": []
}
@@ -799,7 +806,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
# Add summaries to each content item
for content in contents:
- content["summary"] = await self.messageSummarizeContent(content)
+ content["summary"] = await self.getContentExtraction(content)
# Ensure base64Encoded flag is set
if "base64Encoded" not in content:
@@ -861,92 +868,87 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
return preparedInputs
-
- async def messageSummarizeContent(self, content: Dict[str, Any]) -> str:
- return await self.getContentExtraction(
- content,
- "Create a very concise summary (1-2 sentences, maximum 200 characters) about this content."
- )
-
async def processDocumentForAgent(self, document: Dict[str, Any], docSpec: Dict[str, Any]) -> Dict[str, Any]:
- """
- Processes a document for an agent based on the document specification.
- Uses AI to extract relevant content from the document based on the specification.
-
- Args:
- document: The document to process
- docSpec: The document specification from the project manager
+ """
+ Processes a document for an agent based on the document specification.
+ Uses AI to extract relevant content from the document based on the specification.
- Returns:
- Processed document with AI-extracted content
- """
- processedDoc = document.copy()
- partSpec = docSpec.get("contentPart", "")
-
- # Process each content item in the document
- if "contents" in processedDoc:
- processedContents = []
+ Args:
+ document: The document to process
+ docSpec: The document specification from the project manager
+
+ Returns:
+ Processed document with AI-extracted content
+ """
+ processedDoc = document.copy()
+ partSpec = docSpec.get("contentPart", "")
- for content in processedDoc["contents"]:
- # Check if part required
- if partSpec != "" and partSpec != content.get("name"):
- continue
+ # Process each content item in the document
+ if "contents" in processedDoc:
+ processedContents = []
+
+ for content in processedDoc["contents"]:
+ # Check if part required
+ if partSpec != "" and partSpec != content.get("name"):
+ continue
- # Get the prompt from the document specification
- summary = docSpec.get("prompt", "Extract the relevant information from this document")
+ # Get the prompt from the document specification
+ summary = docSpec.get("prompt", "Extract the relevant information from this document")
+
+ # Process content using the shared helper function
+ processedContent = content.copy()
+ processedContent["dataExtracted"] = await self.getContentExtraction(content, summary)
+ processedContent["metadata"]["aiProcessed"] = True
+
+ processedContents.append(processedContent)
- # Process content using the shared helper function
- processedContent = content.copy()
- processedContent["dataExtracted"] = await self.getContentExtraction(content, summary)
- processedContent["metadata"]["aiProcessed"] = True
-
- processedContents.append(processedContent)
+ processedDoc["contents"] = processedContents
- processedDoc["contents"] = processedContents
-
- return processedDoc
+ return processedDoc
async def getContentExtraction(self, content: Dict[str, Any], prompt: str = None) -> str:
"""
- Helper function that extracts or summarizes content based on its type (text/image/binary).
+ Helper function that extracts or summarizes content based on its encoding.
+ For base64 encoded content, uses callAi4Image. For non-base64 content, uses callAi.
Args:
content: Content item to analyze
- prompt: Optional custom prompt for extraction (default prompts used if not provided)
+ prompt: Custom prompt for extraction (default prompts used if not provided)
Returns:
Extracted or summarized content as text
"""
- # Extract relevant information
- data = content.get("data", "")
- contentType = content.get("contentType", "text/plain")
- base64Encoded = content.get("base64Encoded", False)
-
- # Default prompts if none provided
- if prompt is None:
- text_prompt = "Create a very concise summary (1-2 sentences, maximum 200 characters) about this content."
- image_prompt = "Create a very concise summary (1-2 sentences, maximum 200 characters) about this image."
- else:
- text_prompt = prompt
- image_prompt = prompt
-
try:
- # For image content, use the specialized image analysis
- if base64Encoded:
- return await self.mydom.callAi4Image(data, contentType, image_prompt)
-
- # For text data, use the regular AI processing
- else:
- return await self.mydom.callAi([
- {"role": "system", "content": "You are a content analyzer. Process the provided content as instructed."},
- {"role": "user", "content": f"{text_prompt}\n\n{data}"}
- ])
+ # Get content data and encoding status
+ data = content.get("data", "")
+ isBase64 = content.get("base64Encoded", False)
+ # Default prompts if none provided
+ if prompt is None:
+ textPrompt = "Create a very concise summary (1-2 sentences, maximum 200 characters) about this content."
+ imagePrompt = "Create a very concise summary (1-2 sentences, maximum 200 characters) about this image."
+ else:
+ textPrompt = prompt
+ imagePrompt = prompt
+
+ # Handle base64 encoded content
+ if isBase64:
+ try:
+ # Pass base64 encoded data directly to callAi4Image
+ return await self.mydom.callAi4Image(data, content.get("mimeType", "application/octet-stream"), imagePrompt)
+ except Exception as e:
+ logger.error(f"Error processing base64 content: {str(e)}")
+ return f"Error processing content: {str(e)}"
+ else:
+ # For non-base64 content, use callAi
+ return await self.mydom.callAi([
+ {"role": "system", "content": "You are a content analyzer. Extract relevant information from the provided content."},
+ {"role": "user", "content": f"{textPrompt}\n\nContent:\n{data}"}
+ ], produceUserAnswer=True)
+
except Exception as e:
logger.error(f"Error processing content: {str(e)}")
- return f"Content of type {contentType} (processing failed)"
-
-
+ return f"Error processing content: {str(e)}"
def messageAdd(self, workflow: Dict[str, Any], message: Dict[str, Any]) -> Dict[str, Any]:
"""
@@ -1086,56 +1088,69 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
List of file IDs for the saved documents
"""
fileIds = []
+ used_names = set() # Track used names to prevent duplicates
# Extract documents from agent results
documents = agentResults.get("documents", [])
for doc in documents:
try:
- # Extract label (filename) and content
- label = doc.get("label", "unnamed_file.txt")
- content = doc.get("content", "")
+ # Extract document data according to LucyDOM model
+ name = doc.get("name", "")
+ ext = doc.get("ext", "")
+ data = doc.get("data", "")
base64Encoded = doc.get("base64Encoded", False)
- # Split label into name and extension
- name, ext = os.path.splitext(label)
- if ext.startswith('.'):
- ext = ext[1:] # Remove leading dot
- elif not ext:
- # If no extension is provided, default to .txt for text content
- ext = "txt"
- label = f"{label}.{ext}"
+ # Skip if no name or data
+ if not name or not data:
+ logger.warning(f"Skipping document with missing name or data. Name: {name}, Has data: {bool(data)}")
+ continue
+
+ # Ensure unique filename
+ base_name = name
+ counter = 1
+ while f"{base_name}.{ext}" in used_names:
+ base_name = f"{name}_{counter}"
+ counter += 1
+ used_names.add(f"{base_name}.{ext}")
# Convert content to bytes based on base64Encoded flag
- if isinstance(content, str):
+ if isinstance(data, str):
if base64Encoded:
# Decode base64 to bytes
try:
import base64
- fileContent = base64.b64decode(content)
+ fileContent = base64.b64decode(data)
except Exception as e:
logger.warning(f"Failed to decode base64 content: {str(e)}")
- fileContent = content.encode('utf-8')
+ fileContent = data.encode('utf-8')
base64Encoded = False
else:
# Convert text to bytes
- fileContent = content.encode('utf-8')
+ fileContent = data.encode('utf-8')
else:
# Already bytes
- fileContent = content
+ fileContent = data
# Determine MIME type based on extension
- mimeType = self.mydom.getMimeType(label)
+ mimeType = self.mydom.getMimeType(f"{base_name}.{ext}")
- # Save file to database
- fileMeta = self.mydom.saveUploadedFile(fileContent, label)
+ # Create file metadata
+ fileMeta = self.mydom.createFile(
+ name=base_name,
+ mimeType=mimeType,
+ size=len(fileContent)
+ )
if fileMeta and "id" in fileMeta:
- fileId = fileMeta["id"]
- fileIds.append(fileId)
- logger.info(f"Saved document '{label}' with file ID: {fileId} (base64Encoded: {base64Encoded})")
+ # Save file content
+ if self.mydom.createFileData(fileMeta["id"], fileContent):
+ fileIds.append(fileMeta["id"])
+ logger.info(f"Saved document '{base_name}.{ext}' with file ID: {fileMeta['id']} (base64Encoded: {base64Encoded})")
+ else:
+ logger.warning(f"Failed to save content for document '{base_name}.{ext}'")
else:
- logger.warning(f"Failed to save document '{label}'")
+ logger.warning(f"Failed to create file metadata for '{base_name}.{ext}'")
except Exception as e:
logger.error(f"Error saving document from agent results: {str(e)}")
@@ -1174,11 +1189,19 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
# Extract summaries from all contents
contentSummaries = []
- for content in doc.get("contents", []):
+ if "contents" in doc and doc["contents"]:
+ for content in doc["contents"]:
+ contentSummaries.append({
+ "contentPart": content.get("name", "noname"),
+ "metadata": content.get("metadata", ""),
+ "summary": content.get("summary", "No summary"),
+ })
+ else:
+ # Add a default content summary if no contents exist
contentSummaries.append({
- "contentPart": content.get("name", "noname"),
- "metadata": content.get("metadata", ""),
- "summary": content.get("summary", "No summary"),
+ "contentPart": "1_undefined",
+ "metadata": "",
+ "summary": "No content extracted",
})
# Create document info
@@ -1277,11 +1300,12 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
# Singleton factory for the WorkflowManager
_workflowManagers = {}
+_workflowManagerLastAccess = {} # Track last access time for cleanup
def getWorkflowManager(mandateId: int = 0, userId: int = 0) -> WorkflowManager:
"""
Returns a WorkflowManager for the specified context.
- Reuses existing instances.
+ Reuses existing instances but implements cleanup for inactive instances.
Args:
mandateId: ID of the mandate
@@ -1291,6 +1315,32 @@ def getWorkflowManager(mandateId: int = 0, userId: int = 0) -> WorkflowManager:
WorkflowManager instance
"""
contextKey = f"{mandateId}_{userId}"
+ current_time = datetime.now()
+
+ # Update last access time
+ _workflowManagerLastAccess[contextKey] = current_time
+
+ # Cleanup old instances (older than 1 hour)
+ cleanup_threshold = current_time - timedelta(hours=1)
+ for key in list(_workflowManagers.keys()):
+ if _workflowManagerLastAccess.get(key, current_time) < cleanup_threshold:
+ del _workflowManagers[key]
+ del _workflowManagerLastAccess[key]
+
if contextKey not in _workflowManagers:
_workflowManagers[contextKey] = WorkflowManager(mandateId, userId)
- return _workflowManagers[contextKey]
\ No newline at end of file
+ return _workflowManagers[contextKey]
+
+def cleanupWorkflowManager(mandateId: int, userId: int) -> None:
+ """
+ Explicitly cleanup a WorkflowManager instance.
+
+ Args:
+ mandateId: ID of the mandate
+ userId: ID of the user
+ """
+ contextKey = f"{mandateId}_{userId}"
+ if contextKey in _workflowManagers:
+ del _workflowManagers[contextKey]
+ if contextKey in _workflowManagerLastAccess:
+ del _workflowManagerLastAccess[contextKey]
\ No newline at end of file
diff --git a/notes/changelog.txt b/notes/changelog.txt
index 72a0aafe..1aeb06a9 100644
--- a/notes/changelog.txt
+++ b/notes/changelog.txt
@@ -1,28 +1,28 @@
....................... TASKS
+
+Check data extraction of types!
+
+final message with 100% to give
+
+
+
----------------------- OPEN
PRIO1:
-CHECK: If pictures not displayed to check utf-8 encoding in the base64 string!! general file writing and reading (example with svg)
-
-add connector to myoutlook
+sharepoint connector with document search, content search, content extraction
PRIO2:
-todo an agent for "code writing and editing" connected to the codebase, working in loops over each document...
-
sharepoint connector with document search, content search, content extraction
Split big files into content-parts
Integrate NDA Text as modal form - Data governance agreement by login with checkbox
-frontend to react
-
-frontend: no labels definition
PRIO3:
diff --git a/static/10_email_preview.html b/static/10_email_preview.html
deleted file mode 100644
index c900e097..00000000
--- a/static/10_email_preview.html
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
- Email Preview: Verschiebung des Meetings auf Freitag
-
-
-
-
-
-
-
-
To:
-
peter.muster@domain.com
-
-
-
Subject:
-
Verschiebung des Meetings auf Freitag
-
-
-
Sehr geehrter Herr Muster,
ich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting von 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob dieser neue Termin für Sie passt.
Vielen Dank für Ihr Verständnis.
Mit freundlichen Grüßen,
[Ihr Name]
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/static/11_email_template.json b/static/11_email_template.json
deleted file mode 100644
index bf14e27b..00000000
--- a/static/11_email_template.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "recipient": "peter.muster@domain.com",
- "subject": "Verschiebung des Meetings auf Freitag",
- "plainBody": "Sehr geehrter Herr Muster,\n\nich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting von 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob dieser neue Termin f\u00fcr Sie passt.\n\nVielen Dank f\u00fcr Ihr Verst\u00e4ndnis.\n\nMit freundlichen Gr\u00fc\u00dfen,\n\n[Ihr Name]",
- "htmlBody": "Sehr geehrter Herr Muster,
ich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting von 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob dieser neue Termin f\u00fcr Sie passt.
Vielen Dank f\u00fcr Ihr Verst\u00e4ndnis.
Mit freundlichen Gr\u00fc\u00dfen,
[Ihr Name]
"
-}
\ No newline at end of file
diff --git a/static/12_email_preview.html b/static/12_email_preview.html
deleted file mode 100644
index 87962b01..00000000
--- a/static/12_email_preview.html
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
- Email Preview: Anfrage zur Terminverschiebung
-
-
-
-
-
-
-
-
To:
-
peter.muster@domain.com
-
-
-
Subject:
-
Anfrage zur Terminverschiebung
-
-
-
Sehr geehrter Herr Muster,
ich hoffe, diese Nachricht trifft Sie wohl. Ich schreibe Ihnen, um eine Verschiebung unseres Termins von 10 Uhr auf Freitag zu erbitten. Bitte lassen Sie mich wissen, ob dies für Sie möglich ist.
Vielen Dank im Voraus für Ihre Flexibilität.
Mit freundlichen Grüßen,
[Ihr Name]
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/static/13_email_template.json b/static/13_email_template.json
deleted file mode 100644
index ab8a946c..00000000
--- a/static/13_email_template.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "recipient": "peter.muster@domain.com",
- "subject": "Anfrage zur Terminverschiebung",
- "plainBody": "Sehr geehrter Herr Muster,\n\nich hoffe, diese Nachricht trifft Sie wohl. Ich schreibe Ihnen, um eine Verschiebung unseres Termins von 10 Uhr auf Freitag zu erbitten. Bitte lassen Sie mich wissen, ob dies f\u00fcr Sie m\u00f6glich ist.\n\nVielen Dank im Voraus f\u00fcr Ihre Flexibilit\u00e4t.\n\nMit freundlichen Gr\u00fc\u00dfen,\n\n[Ihr Name]",
- "htmlBody": "Sehr geehrter Herr Muster,
ich hoffe, diese Nachricht trifft Sie wohl. Ich schreibe Ihnen, um eine Verschiebung unseres Termins von 10 Uhr auf Freitag zu erbitten. Bitte lassen Sie mich wissen, ob dies f\u00fcr Sie m\u00f6glich ist.
Vielen Dank im Voraus f\u00fcr Ihre Flexibilit\u00e4t.
Mit freundlichen Gr\u00fc\u00dfen,
[Ihr Name]
"
-}
\ No newline at end of file
diff --git a/static/14_microsoft_authentication.html b/static/14_microsoft_authentication.html
deleted file mode 100644
index b8a50d7f..00000000
--- a/static/14_microsoft_authentication.html
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
-
- Microsoft Authentication Required
-
-
-
-
-
Microsoft Authentication Required
-
-
To create email templates and drafts, you need to authenticate with your Microsoft account. Follow these steps:
-
-
- 1
- Click the authentication link below
-
-
-
Authenticate with Microsoft
-
-
- 2
- Sign in with your Microsoft account and grant the required permissions
-
-
-
- 3
- Return to this application and run the email agent again after completing authentication
-
-
-
-
Note: You only need to authenticate once. Your session will be remembered for future email operations.
-
-
-
-
-
\ No newline at end of file
diff --git a/static/15_microsoft_authentication.html b/static/15_microsoft_authentication.html
deleted file mode 100644
index 521bae1c..00000000
--- a/static/15_microsoft_authentication.html
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
- Microsoft Authentication Required
-
-
-
-
-
Microsoft Authentication Required
-
-
To create email templates and drafts, you need to authenticate with your Microsoft account.
-
-
The application will now initiate the Microsoft authentication process. Please follow the instructions in the authentication window.
-
-
-
Note: You only need to authenticate once. Your session will be remembered for future email operations.
-
-
-
-
-
\ No newline at end of file
diff --git a/static/16_email_preview.html b/static/16_email_preview.html
deleted file mode 100644
index 95096bad..00000000
--- a/static/16_email_preview.html
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
- Email Preview: Verschiebung des Meetings auf Freitag
-
-
-
-
-
-
-
-
To:
-
peter.muster@domain.com
-
-
-
Subject:
-
Verschiebung des Meetings auf Freitag
-
-
-
Sehr geehrter Herr Muster,
ich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting um 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob dieser Termin für Sie passt.
Vielen Dank für Ihr Verständnis.
Mit freundlichen Grüßen,
[Ihr Name]
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/static/17_email_template.json b/static/17_email_template.json
deleted file mode 100644
index 90e8f9f3..00000000
--- a/static/17_email_template.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "recipient": "peter.muster@domain.com",
- "subject": "Verschiebung des Meetings auf Freitag",
- "plainBody": "Sehr geehrter Herr Muster,\n\nich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting um 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob dieser Termin f\u00fcr Sie passt.\n\nVielen Dank f\u00fcr Ihr Verst\u00e4ndnis.\n\nMit freundlichen Gr\u00fc\u00dfen,\n\n[Ihr Name]",
- "htmlBody": "Sehr geehrter Herr Muster,
ich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting um 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob dieser Termin f\u00fcr Sie passt.
Vielen Dank f\u00fcr Ihr Verst\u00e4ndnis.
Mit freundlichen Gr\u00fc\u00dfen,
[Ihr Name]
"
-}
\ No newline at end of file
diff --git a/static/18_generated_code.py b/static/18_generated_code.py
deleted file mode 100644
index b53f58c4..00000000
--- a/static/18_generated_code.py
+++ /dev/null
@@ -1,48 +0,0 @@
-inputFiles = [] # DO NOT CHANGE THIS LINE
-
-# REQUIREMENTS:
-
-import json
-import csv
-from io import StringIO
-
-def is_prime(n):
- if n <= 1:
- return False
- if n <= 3:
- return True
- if n % 2 == 0 or n % 3 == 0:
- return False
- i = 5
- while i * i <= n:
- if n % i == 0 or n % (i + 2) == 0:
- return False
- i += 6
- return True
-
-def generate_primes(limit):
- primes = []
- num = 2
- while len(primes) < limit:
- if is_prime(num):
- primes.append(num)
- num += 1
- return primes
-
-primes = generate_primes(1000)
-
-output = StringIO()
-csv_writer = csv.writer(output)
-for prime in primes:
- csv_writer.writerow([prime])
-
-result = {
- "prime_numbers.csv": {
- "content": output.getvalue(),
- "base64Encoded": False,
- "contentType": "text/csv"
- }
-}
-
-import json
-print(json.dumps(result))
\ No newline at end of file
diff --git a/static/19_execution_history.json b/static/19_execution_history.json
deleted file mode 100644
index 8b61dc57..00000000
--- a/static/19_execution_history.json
+++ /dev/null
@@ -1,19 +0,0 @@
-[
- {
- "attempt": 1,
- "code": "inputFiles = [] # DO NOT CHANGE THIS LINE\n\n# REQUIREMENTS: \n\nimport json\nimport csv\nfrom io import StringIO\n\ndef is_prime(n):\n if n <= 1:\n return False\n if n <= 3:\n return True\n if n % 2 == 0 or n % 3 == 0:\n return False\n i = 5\n while i * i <= n:\n if n % i == 0 or n % (i + 2) == 0:\n return False\n i += 6\n return True\n\ndef generate_primes(limit):\n primes = []\n num = 2\n while len(primes) < limit:\n if is_prime(num):\n primes.append(num)\n num += 1\n return primes\n\nprimes = generate_primes(1000)\n\noutput = StringIO()\ncsv_writer = csv.writer(output)\nfor prime in primes:\n csv_writer.writerow([prime])\n\nresult = {\n \"prime_numbers.csv\": {\n \"content\": output.getvalue(),\n \"base64Encoded\": False,\n \"contentType\": \"text/csv\"\n }\n}\n\nimport json\nprint(json.dumps(result))",
- "result": {
- "success": true,
- "output": "{\"prime_numbers.csv\": {\"content\": \"2\\r\\n3\\r\\n5\\r\\n7\\r\\n11\\r\\n13\\r\\n17\\r\\n19\\r\\n23\\r\\n29\\r\\n31\\r\\n37\\r\\n41\\r\\n43\\r\\n47\\r\\n53\\r\\n59\\r\\n61\\r\\n67\\r\\n71\\r\\n73\\r\\n79\\r\\n83\\r\\n89\\r\\n97\\r\\n101\\r\\n103\\r\\n107\\r\\n109\\r\\n113\\r\\n127\\r\\n131\\r\\n137\\r\\n139\\r\\n149\\r\\n151\\r\\n157\\r\\n163\\r\\n167\\r\\n173\\r\\n179\\r\\n181\\r\\n191\\r\\n193\\r\\n197\\r\\n199\\r\\n211\\r\\n223\\r\\n227\\r\\n229\\r\\n233\\r\\n239\\r\\n241\\r\\n251\\r\\n257\\r\\n263\\r\\n269\\r\\n271\\r\\n277\\r\\n281\\r\\n283\\r\\n293\\r\\n307\\r\\n311\\r\\n313\\r\\n317\\r\\n331\\r\\n337\\r\\n347\\r\\n349\\r\\n353\\r\\n359\\r\\n367\\r\\n373\\r\\n379\\r\\n383\\r\\n389\\r\\n397\\r\\n401\\r\\n409\\r\\n419\\r\\n421\\r\\n431\\r\\n433\\r\\n439\\r\\n443\\r\\n449\\r\\n457\\r\\n461\\r\\n463\\r\\n467\\r\\n479\\r\\n487\\r\\n491\\r\\n499\\r\\n503\\r\\n509\\r\\n521\\r\\n523\\r\\n541\\r\\n547\\r\\n557\\r\\n563\\r\\n569\\r\\n571\\r\\n577\\r\\n587\\r\\n593\\r\\n599\\r\\n601\\r\\n607\\r\\n613\\r\\n617\\r\\n619\\r\\n631\\r\\n641\\r\\n643\\r\\n647\\r\\n653\\r\\n659\\r\\n661\\r\\n673\\r\\n677\\r\\n683\\r\\n691\\r\\n701\\r\\n709\\r\\n719\\r\\n727\\r\\n733\\r\\n739\\r\\n743\\r\\n751\\r\\n757\\r\\n761\\r\\n769\\r\\n773\\r\\n787\\r\\n797\\r\\n809\\r\\n811\\r\\n821\\r\\n823\\r\\n827\\r\\n829\\r\\n839\\r\\n853\\r\\n857\\r\\n859\\r\\n863\\r\\n877\\r\\n881\\r\\n883\\r\\n887\\r\\n907\\r\\n911\\r\\n919\\r\\n929\\r\\n937\\r\\n941\\r\\n947\\r\\n953\\r\\n967\\r\\n971\\r\\n977\\r\\n983\\r\\n991\\r\\n997\\r\\n1009\\r\\n1013\\r\\n1019\\r\\n1021\\r\\n1031\\r\\n1033\\r\\n1039\\r\\n1049\\r\\n1051\\r\\n1061\\r\\n1063\\r\\n1069\\r\\n1087\\r\\n1091\\r\\n1093\\r\\n1097\\r\\n1103\\r\\n1109\\r\\n1117\\r\\n1123\\r\\n1129\\r\\n1151\\r\\n1153\\r\\n1163\\r\\n1171\\r\\n1181\\r\\n1187\\r\\n1193\\r\\n1201\\r\\n1213\\r\\n1217\\r\\n1223\\r\\n1229\\r\\n1231\\r\\n1237\\r\\n1249\\r\\n1259\\r\\n1277\\r\\n1279\\r\\n1283\\r\\n1289\\r\\n1291\\r\\n1297\\r\\n1301\\r\\n1303\\r\\n1307\\r\\n1319\\r\\n1321\\r\\n1327\\r\\n1361\\r\\n1367\\r\\n1373\\r\\n1381\\r\\n1399\\r\\n1409\\r\\n1423\\r\\n1427\\r\\n1429\\r\\n1433\\r\\n1439\\r\\n1447\\r\\n1451\\r\\n1453\\r\\n1459\\r\\n1471\\r\\n1481\\r\\n1483\\r\\n1487\\r\\n1489\\r\\n1493\\r\\n1499\\r\\n1511\\r\\n1523\\r\\n1531\\r\\n1543\\r\\n1549\\r\\n1553\\r\\n1559\\r\\n1567\\r\\n1571\\r\\n1579\\r\\n1583\\r\\n1597\\r\\n1601\\r\\n1607\\r\\n1609\\r\\n1613\\r\\n1619\\r\\n1621\\r\\n1627\\r\\n1637\\r\\n1657\\r\\n1663\\r\\n1667\\r\\n1669\\r\\n1693\\r\\n1697\\r\\n1699\\r\\n1709\\r\\n1721\\r\\n1723\\r\\n1733\\r\\n1741\\r\\n1747\\r\\n1753\\r\\n1759\\r\\n1777\\r\\n1783\\r\\n1787\\r\\n1789\\r\\n1801\\r\\n1811\\r\\n1823\\r\\n1831\\r\\n1847\\r\\n1861\\r\\n1867\\r\\n1871\\r\\n1873\\r\\n1877\\r\\n1879\\r\\n1889\\r\\n1901\\r\\n1907\\r\\n1913\\r\\n1931\\r\\n1933\\r\\n1949\\r\\n1951\\r\\n1973\\r\\n1979\\r\\n1987\\r\\n1993\\r\\n1997\\r\\n1999\\r\\n2003\\r\\n2011\\r\\n2017\\r\\n2027\\r\\n2029\\r\\n2039\\r\\n2053\\r\\n2063\\r\\n2069\\r\\n2081\\r\\n2083\\r\\n2087\\r\\n2089\\r\\n2099\\r\\n2111\\r\\n2113\\r\\n2129\\r\\n2131\\r\\n2137\\r\\n2141\\r\\n2143\\r\\n2153\\r\\n2161\\r\\n2179\\r\\n2203\\r\\n2207\\r\\n2213\\r\\n2221\\r\\n2237\\r\\n2239\\r\\n2243\\r\\n2251\\r\\n2267\\r\\n2269\\r\\n2273\\r\\n2281\\r\\n2287\\r\\n2293\\r\\n2297\\r\\n2309\\r\\n2311\\r\\n2333\\r\\n2339\\r\\n2341\\r\\n2347\\r\\n2351\\r\\n2357\\r\\n2371\\r\\n2377\\r\\n2381\\r\\n2383\\r\\n2389\\r\\n2393\\r\\n2399\\r\\n2411\\r\\n2417\\r\\n2423\\r\\n2437\\r\\n2441\\r\\n2447\\r\\n2459\\r\\n2467\\r\\n2473\\r\\n2477\\r\\n2503\\r\\n2521\\r\\n2531\\r\\n2539\\r\\n2543\\r\\n2549\\r\\n2551\\r\\n2557\\r\\n2579\\r\\n2591\\r\\n2593\\r\\n2609\\r\\n2617\\r\\n2621\\r\\n2633\\r\\n2647\\r\\n2657\\r\\n2659\\r\\n2663\\r\\n2671\\r\\n2677\\r\\n2683\\r\\n2687\\r\\n2689\\r\\n2693\\r\\n2699\\r\\n2707\\r\\n2711\\r\\n2713\\r\\n2719\\r\\n2729\\r\\n2731\\r\\n2741\\r\\n2749\\r\\n2753\\r\\n2767\\r\\n2777\\r\\n2789\\r\\n2791\\r\\n2797\\r\\n2801\\r\\n2803\\r\\n2819\\r\\n2833\\r\\n2837\\r\\n2843\\r\\n2851\\r\\n2857\\r\\n2861\\r\\n2879\\r\\n2887\\r\\n2897\\r\\n2903\\r\\n2909\\r\\n2917\\r\\n2927\\r\\n2939\\r\\n2953\\r\\n2957\\r\\n2963\\r\\n2969\\r\\n2971\\r\\n2999\\r\\n3001\\r\\n3011\\r\\n3019\\r\\n3023\\r\\n3037\\r\\n3041\\r\\n3049\\r\\n3061\\r\\n3067\\r\\n3079\\r\\n3083\\r\\n3089\\r\\n3109\\r\\n3119\\r\\n3121\\r\\n3137\\r\\n3163\\r\\n3167\\r\\n3169\\r\\n3181\\r\\n3187\\r\\n3191\\r\\n3203\\r\\n3209\\r\\n3217\\r\\n3221\\r\\n3229\\r\\n3251\\r\\n3253\\r\\n3257\\r\\n3259\\r\\n3271\\r\\n3299\\r\\n3301\\r\\n3307\\r\\n3313\\r\\n3319\\r\\n3323\\r\\n3329\\r\\n3331\\r\\n3343\\r\\n3347\\r\\n3359\\r\\n3361\\r\\n3371\\r\\n3373\\r\\n3389\\r\\n3391\\r\\n3407\\r\\n3413\\r\\n3433\\r\\n3449\\r\\n3457\\r\\n3461\\r\\n3463\\r\\n3467\\r\\n3469\\r\\n3491\\r\\n3499\\r\\n3511\\r\\n3517\\r\\n3527\\r\\n3529\\r\\n3533\\r\\n3539\\r\\n3541\\r\\n3547\\r\\n3557\\r\\n3559\\r\\n3571\\r\\n3581\\r\\n3583\\r\\n3593\\r\\n3607\\r\\n3613\\r\\n3617\\r\\n3623\\r\\n3631\\r\\n3637\\r\\n3643\\r\\n3659\\r\\n3671\\r\\n3673\\r\\n3677\\r\\n3691\\r\\n3697\\r\\n3701\\r\\n3709\\r\\n3719\\r\\n3727\\r\\n3733\\r\\n3739\\r\\n3761\\r\\n3767\\r\\n3769\\r\\n3779\\r\\n3793\\r\\n3797\\r\\n3803\\r\\n3821\\r\\n3823\\r\\n3833\\r\\n3847\\r\\n3851\\r\\n3853\\r\\n3863\\r\\n3877\\r\\n3881\\r\\n3889\\r\\n3907\\r\\n3911\\r\\n3917\\r\\n3919\\r\\n3923\\r\\n3929\\r\\n3931\\r\\n3943\\r\\n3947\\r\\n3967\\r\\n3989\\r\\n4001\\r\\n4003\\r\\n4007\\r\\n4013\\r\\n4019\\r\\n4021\\r\\n4027\\r\\n4049\\r\\n4051\\r\\n4057\\r\\n4073\\r\\n4079\\r\\n4091\\r\\n4093\\r\\n4099\\r\\n4111\\r\\n4127\\r\\n4129\\r\\n4133\\r\\n4139\\r\\n4153\\r\\n4157\\r\\n4159\\r\\n4177\\r\\n4201\\r\\n4211\\r\\n4217\\r\\n4219\\r\\n4229\\r\\n4231\\r\\n4241\\r\\n4243\\r\\n4253\\r\\n4259\\r\\n4261\\r\\n4271\\r\\n4273\\r\\n4283\\r\\n4289\\r\\n4297\\r\\n4327\\r\\n4337\\r\\n4339\\r\\n4349\\r\\n4357\\r\\n4363\\r\\n4373\\r\\n4391\\r\\n4397\\r\\n4409\\r\\n4421\\r\\n4423\\r\\n4441\\r\\n4447\\r\\n4451\\r\\n4457\\r\\n4463\\r\\n4481\\r\\n4483\\r\\n4493\\r\\n4507\\r\\n4513\\r\\n4517\\r\\n4519\\r\\n4523\\r\\n4547\\r\\n4549\\r\\n4561\\r\\n4567\\r\\n4583\\r\\n4591\\r\\n4597\\r\\n4603\\r\\n4621\\r\\n4637\\r\\n4639\\r\\n4643\\r\\n4649\\r\\n4651\\r\\n4657\\r\\n4663\\r\\n4673\\r\\n4679\\r\\n4691\\r\\n4703\\r\\n4721\\r\\n4723\\r\\n4729\\r\\n4733\\r\\n4751\\r\\n4759\\r\\n4783\\r\\n4787\\r\\n4789\\r\\n4793\\r\\n4799\\r\\n4801\\r\\n4813\\r\\n4817\\r\\n4831\\r\\n4861\\r\\n4871\\r\\n4877\\r\\n4889\\r\\n4903\\r\\n4909\\r\\n4919\\r\\n4931\\r\\n4933\\r\\n4937\\r\\n4943\\r\\n4951\\r\\n4957\\r\\n4967\\r\\n4969\\r\\n4973\\r\\n4987\\r\\n4993\\r\\n4999\\r\\n5003\\r\\n5009\\r\\n5011\\r\\n5021\\r\\n5023\\r\\n5039\\r\\n5051\\r\\n5059\\r\\n5077\\r\\n5081\\r\\n5087\\r\\n5099\\r\\n5101\\r\\n5107\\r\\n5113\\r\\n5119\\r\\n5147\\r\\n5153\\r\\n5167\\r\\n5171\\r\\n5179\\r\\n5189\\r\\n5197\\r\\n5209\\r\\n5227\\r\\n5231\\r\\n5233\\r\\n5237\\r\\n5261\\r\\n5273\\r\\n5279\\r\\n5281\\r\\n5297\\r\\n5303\\r\\n5309\\r\\n5323\\r\\n5333\\r\\n5347\\r\\n5351\\r\\n5381\\r\\n5387\\r\\n5393\\r\\n5399\\r\\n5407\\r\\n5413\\r\\n5417\\r\\n5419\\r\\n5431\\r\\n5437\\r\\n5441\\r\\n5443\\r\\n5449\\r\\n5471\\r\\n5477\\r\\n5479\\r\\n5483\\r\\n5501\\r\\n5503\\r\\n5507\\r\\n5519\\r\\n5521\\r\\n5527\\r\\n5531\\r\\n5557\\r\\n5563\\r\\n5569\\r\\n5573\\r\\n5581\\r\\n5591\\r\\n5623\\r\\n5639\\r\\n5641\\r\\n5647\\r\\n5651\\r\\n5653\\r\\n5657\\r\\n5659\\r\\n5669\\r\\n5683\\r\\n5689\\r\\n5693\\r\\n5701\\r\\n5711\\r\\n5717\\r\\n5737\\r\\n5741\\r\\n5743\\r\\n5749\\r\\n5779\\r\\n5783\\r\\n5791\\r\\n5801\\r\\n5807\\r\\n5813\\r\\n5821\\r\\n5827\\r\\n5839\\r\\n5843\\r\\n5849\\r\\n5851\\r\\n5857\\r\\n5861\\r\\n5867\\r\\n5869\\r\\n5879\\r\\n5881\\r\\n5897\\r\\n5903\\r\\n5923\\r\\n5927\\r\\n5939\\r\\n5953\\r\\n5981\\r\\n5987\\r\\n6007\\r\\n6011\\r\\n6029\\r\\n6037\\r\\n6043\\r\\n6047\\r\\n6053\\r\\n6067\\r\\n6073\\r\\n6079\\r\\n6089\\r\\n6091\\r\\n6101\\r\\n6113\\r\\n6121\\r\\n6131\\r\\n6133\\r\\n6143\\r\\n6151\\r\\n6163\\r\\n6173\\r\\n6197\\r\\n6199\\r\\n6203\\r\\n6211\\r\\n6217\\r\\n6221\\r\\n6229\\r\\n6247\\r\\n6257\\r\\n6263\\r\\n6269\\r\\n6271\\r\\n6277\\r\\n6287\\r\\n6299\\r\\n6301\\r\\n6311\\r\\n6317\\r\\n6323\\r\\n6329\\r\\n6337\\r\\n6343\\r\\n6353\\r\\n6359\\r\\n6361\\r\\n6367\\r\\n6373\\r\\n6379\\r\\n6389\\r\\n6397\\r\\n6421\\r\\n6427\\r\\n6449\\r\\n6451\\r\\n6469\\r\\n6473\\r\\n6481\\r\\n6491\\r\\n6521\\r\\n6529\\r\\n6547\\r\\n6551\\r\\n6553\\r\\n6563\\r\\n6569\\r\\n6571\\r\\n6577\\r\\n6581\\r\\n6599\\r\\n6607\\r\\n6619\\r\\n6637\\r\\n6653\\r\\n6659\\r\\n6661\\r\\n6673\\r\\n6679\\r\\n6689\\r\\n6691\\r\\n6701\\r\\n6703\\r\\n6709\\r\\n6719\\r\\n6733\\r\\n6737\\r\\n6761\\r\\n6763\\r\\n6779\\r\\n6781\\r\\n6791\\r\\n6793\\r\\n6803\\r\\n6823\\r\\n6827\\r\\n6829\\r\\n6833\\r\\n6841\\r\\n6857\\r\\n6863\\r\\n6869\\r\\n6871\\r\\n6883\\r\\n6899\\r\\n6907\\r\\n6911\\r\\n6917\\r\\n6947\\r\\n6949\\r\\n6959\\r\\n6961\\r\\n6967\\r\\n6971\\r\\n6977\\r\\n6983\\r\\n6991\\r\\n6997\\r\\n7001\\r\\n7013\\r\\n7019\\r\\n7027\\r\\n7039\\r\\n7043\\r\\n7057\\r\\n7069\\r\\n7079\\r\\n7103\\r\\n7109\\r\\n7121\\r\\n7127\\r\\n7129\\r\\n7151\\r\\n7159\\r\\n7177\\r\\n7187\\r\\n7193\\r\\n7207\\r\\n7211\\r\\n7213\\r\\n7219\\r\\n7229\\r\\n7237\\r\\n7243\\r\\n7247\\r\\n7253\\r\\n7283\\r\\n7297\\r\\n7307\\r\\n7309\\r\\n7321\\r\\n7331\\r\\n7333\\r\\n7349\\r\\n7351\\r\\n7369\\r\\n7393\\r\\n7411\\r\\n7417\\r\\n7433\\r\\n7451\\r\\n7457\\r\\n7459\\r\\n7477\\r\\n7481\\r\\n7487\\r\\n7489\\r\\n7499\\r\\n7507\\r\\n7517\\r\\n7523\\r\\n7529\\r\\n7537\\r\\n7541\\r\\n7547\\r\\n7549\\r\\n7559\\r\\n7561\\r\\n7573\\r\\n7577\\r\\n7583\\r\\n7589\\r\\n7591\\r\\n7603\\r\\n7607\\r\\n7621\\r\\n7639\\r\\n7643\\r\\n7649\\r\\n7669\\r\\n7673\\r\\n7681\\r\\n7687\\r\\n7691\\r\\n7699\\r\\n7703\\r\\n7717\\r\\n7723\\r\\n7727\\r\\n7741\\r\\n7753\\r\\n7757\\r\\n7759\\r\\n7789\\r\\n7793\\r\\n7817\\r\\n7823\\r\\n7829\\r\\n7841\\r\\n7853\\r\\n7867\\r\\n7873\\r\\n7877\\r\\n7879\\r\\n7883\\r\\n7901\\r\\n7907\\r\\n7919\\r\\n\", \"base64Encoded\": false, \"contentType\": \"text/csv\"}}\n",
- "error": "",
- "result": {
- "prime_numbers.csv": {
- "content": "2\r\n3\r\n5\r\n7\r\n11\r\n13\r\n17\r\n19\r\n23\r\n29\r\n31\r\n37\r\n41\r\n43\r\n47\r\n53\r\n59\r\n61\r\n67\r\n71\r\n73\r\n79\r\n83\r\n89\r\n97\r\n101\r\n103\r\n107\r\n109\r\n113\r\n127\r\n131\r\n137\r\n139\r\n149\r\n151\r\n157\r\n163\r\n167\r\n173\r\n179\r\n181\r\n191\r\n193\r\n197\r\n199\r\n211\r\n223\r\n227\r\n229\r\n233\r\n239\r\n241\r\n251\r\n257\r\n263\r\n269\r\n271\r\n277\r\n281\r\n283\r\n293\r\n307\r\n311\r\n313\r\n317\r\n331\r\n337\r\n347\r\n349\r\n353\r\n359\r\n367\r\n373\r\n379\r\n383\r\n389\r\n397\r\n401\r\n409\r\n419\r\n421\r\n431\r\n433\r\n439\r\n443\r\n449\r\n457\r\n461\r\n463\r\n467\r\n479\r\n487\r\n491\r\n499\r\n503\r\n509\r\n521\r\n523\r\n541\r\n547\r\n557\r\n563\r\n569\r\n571\r\n577\r\n587\r\n593\r\n599\r\n601\r\n607\r\n613\r\n617\r\n619\r\n631\r\n641\r\n643\r\n647\r\n653\r\n659\r\n661\r\n673\r\n677\r\n683\r\n691\r\n701\r\n709\r\n719\r\n727\r\n733\r\n739\r\n743\r\n751\r\n757\r\n761\r\n769\r\n773\r\n787\r\n797\r\n809\r\n811\r\n821\r\n823\r\n827\r\n829\r\n839\r\n853\r\n857\r\n859\r\n863\r\n877\r\n881\r\n883\r\n887\r\n907\r\n911\r\n919\r\n929\r\n937\r\n941\r\n947\r\n953\r\n967\r\n971\r\n977\r\n983\r\n991\r\n997\r\n1009\r\n1013\r\n1019\r\n1021\r\n1031\r\n1033\r\n1039\r\n1049\r\n1051\r\n1061\r\n1063\r\n1069\r\n1087\r\n1091\r\n1093\r\n1097\r\n1103\r\n1109\r\n1117\r\n1123\r\n1129\r\n1151\r\n1153\r\n1163\r\n1171\r\n1181\r\n1187\r\n1193\r\n1201\r\n1213\r\n1217\r\n1223\r\n1229\r\n1231\r\n1237\r\n1249\r\n1259\r\n1277\r\n1279\r\n1283\r\n1289\r\n1291\r\n1297\r\n1301\r\n1303\r\n1307\r\n1319\r\n1321\r\n1327\r\n1361\r\n1367\r\n1373\r\n1381\r\n1399\r\n1409\r\n1423\r\n1427\r\n1429\r\n1433\r\n1439\r\n1447\r\n1451\r\n1453\r\n1459\r\n1471\r\n1481\r\n1483\r\n1487\r\n1489\r\n1493\r\n1499\r\n1511\r\n1523\r\n1531\r\n1543\r\n1549\r\n1553\r\n1559\r\n1567\r\n1571\r\n1579\r\n1583\r\n1597\r\n1601\r\n1607\r\n1609\r\n1613\r\n1619\r\n1621\r\n1627\r\n1637\r\n1657\r\n1663\r\n1667\r\n1669\r\n1693\r\n1697\r\n1699\r\n1709\r\n1721\r\n1723\r\n1733\r\n1741\r\n1747\r\n1753\r\n1759\r\n1777\r\n1783\r\n1787\r\n1789\r\n1801\r\n1811\r\n1823\r\n1831\r\n1847\r\n1861\r\n1867\r\n1871\r\n1873\r\n1877\r\n1879\r\n1889\r\n1901\r\n1907\r\n1913\r\n1931\r\n1933\r\n1949\r\n1951\r\n1973\r\n1979\r\n1987\r\n1993\r\n1997\r\n1999\r\n2003\r\n2011\r\n2017\r\n2027\r\n2029\r\n2039\r\n2053\r\n2063\r\n2069\r\n2081\r\n2083\r\n2087\r\n2089\r\n2099\r\n2111\r\n2113\r\n2129\r\n2131\r\n2137\r\n2141\r\n2143\r\n2153\r\n2161\r\n2179\r\n2203\r\n2207\r\n2213\r\n2221\r\n2237\r\n2239\r\n2243\r\n2251\r\n2267\r\n2269\r\n2273\r\n2281\r\n2287\r\n2293\r\n2297\r\n2309\r\n2311\r\n2333\r\n2339\r\n2341\r\n2347\r\n2351\r\n2357\r\n2371\r\n2377\r\n2381\r\n2383\r\n2389\r\n2393\r\n2399\r\n2411\r\n2417\r\n2423\r\n2437\r\n2441\r\n2447\r\n2459\r\n2467\r\n2473\r\n2477\r\n2503\r\n2521\r\n2531\r\n2539\r\n2543\r\n2549\r\n2551\r\n2557\r\n2579\r\n2591\r\n2593\r\n2609\r\n2617\r\n2621\r\n2633\r\n2647\r\n2657\r\n2659\r\n2663\r\n2671\r\n2677\r\n2683\r\n2687\r\n2689\r\n2693\r\n2699\r\n2707\r\n2711\r\n2713\r\n2719\r\n2729\r\n2731\r\n2741\r\n2749\r\n2753\r\n2767\r\n2777\r\n2789\r\n2791\r\n2797\r\n2801\r\n2803\r\n2819\r\n2833\r\n2837\r\n2843\r\n2851\r\n2857\r\n2861\r\n2879\r\n2887\r\n2897\r\n2903\r\n2909\r\n2917\r\n2927\r\n2939\r\n2953\r\n2957\r\n2963\r\n2969\r\n2971\r\n2999\r\n3001\r\n3011\r\n3019\r\n3023\r\n3037\r\n3041\r\n3049\r\n3061\r\n3067\r\n3079\r\n3083\r\n3089\r\n3109\r\n3119\r\n3121\r\n3137\r\n3163\r\n3167\r\n3169\r\n3181\r\n3187\r\n3191\r\n3203\r\n3209\r\n3217\r\n3221\r\n3229\r\n3251\r\n3253\r\n3257\r\n3259\r\n3271\r\n3299\r\n3301\r\n3307\r\n3313\r\n3319\r\n3323\r\n3329\r\n3331\r\n3343\r\n3347\r\n3359\r\n3361\r\n3371\r\n3373\r\n3389\r\n3391\r\n3407\r\n3413\r\n3433\r\n3449\r\n3457\r\n3461\r\n3463\r\n3467\r\n3469\r\n3491\r\n3499\r\n3511\r\n3517\r\n3527\r\n3529\r\n3533\r\n3539\r\n3541\r\n3547\r\n3557\r\n3559\r\n3571\r\n3581\r\n3583\r\n3593\r\n3607\r\n3613\r\n3617\r\n3623\r\n3631\r\n3637\r\n3643\r\n3659\r\n3671\r\n3673\r\n3677\r\n3691\r\n3697\r\n3701\r\n3709\r\n3719\r\n3727\r\n3733\r\n3739\r\n3761\r\n3767\r\n3769\r\n3779\r\n3793\r\n3797\r\n3803\r\n3821\r\n3823\r\n3833\r\n3847\r\n3851\r\n3853\r\n3863\r\n3877\r\n3881\r\n3889\r\n3907\r\n3911\r\n3917\r\n3919\r\n3923\r\n3929\r\n3931\r\n3943\r\n3947\r\n3967\r\n3989\r\n4001\r\n4003\r\n4007\r\n4013\r\n4019\r\n4021\r\n4027\r\n4049\r\n4051\r\n4057\r\n4073\r\n4079\r\n4091\r\n4093\r\n4099\r\n4111\r\n4127\r\n4129\r\n4133\r\n4139\r\n4153\r\n4157\r\n4159\r\n4177\r\n4201\r\n4211\r\n4217\r\n4219\r\n4229\r\n4231\r\n4241\r\n4243\r\n4253\r\n4259\r\n4261\r\n4271\r\n4273\r\n4283\r\n4289\r\n4297\r\n4327\r\n4337\r\n4339\r\n4349\r\n4357\r\n4363\r\n4373\r\n4391\r\n4397\r\n4409\r\n4421\r\n4423\r\n4441\r\n4447\r\n4451\r\n4457\r\n4463\r\n4481\r\n4483\r\n4493\r\n4507\r\n4513\r\n4517\r\n4519\r\n4523\r\n4547\r\n4549\r\n4561\r\n4567\r\n4583\r\n4591\r\n4597\r\n4603\r\n4621\r\n4637\r\n4639\r\n4643\r\n4649\r\n4651\r\n4657\r\n4663\r\n4673\r\n4679\r\n4691\r\n4703\r\n4721\r\n4723\r\n4729\r\n4733\r\n4751\r\n4759\r\n4783\r\n4787\r\n4789\r\n4793\r\n4799\r\n4801\r\n4813\r\n4817\r\n4831\r\n4861\r\n4871\r\n4877\r\n4889\r\n4903\r\n4909\r\n4919\r\n4931\r\n4933\r\n4937\r\n4943\r\n4951\r\n4957\r\n4967\r\n4969\r\n4973\r\n4987\r\n4993\r\n4999\r\n5003\r\n5009\r\n5011\r\n5021\r\n5023\r\n5039\r\n5051\r\n5059\r\n5077\r\n5081\r\n5087\r\n5099\r\n5101\r\n5107\r\n5113\r\n5119\r\n5147\r\n5153\r\n5167\r\n5171\r\n5179\r\n5189\r\n5197\r\n5209\r\n5227\r\n5231\r\n5233\r\n5237\r\n5261\r\n5273\r\n5279\r\n5281\r\n5297\r\n5303\r\n5309\r\n5323\r\n5333\r\n5347\r\n5351\r\n5381\r\n5387\r\n5393\r\n5399\r\n5407\r\n5413\r\n5417\r\n5419\r\n5431\r\n5437\r\n5441\r\n5443\r\n5449\r\n5471\r\n5477\r\n5479\r\n5483\r\n5501\r\n5503\r\n5507\r\n5519\r\n5521\r\n5527\r\n5531\r\n5557\r\n5563\r\n5569\r\n5573\r\n5581\r\n5591\r\n5623\r\n5639\r\n5641\r\n5647\r\n5651\r\n5653\r\n5657\r\n5659\r\n5669\r\n5683\r\n5689\r\n5693\r\n5701\r\n5711\r\n5717\r\n5737\r\n5741\r\n5743\r\n5749\r\n5779\r\n5783\r\n5791\r\n5801\r\n5807\r\n5813\r\n5821\r\n5827\r\n5839\r\n5843\r\n5849\r\n5851\r\n5857\r\n5861\r\n5867\r\n5869\r\n5879\r\n5881\r\n5897\r\n5903\r\n5923\r\n5927\r\n5939\r\n5953\r\n5981\r\n5987\r\n6007\r\n6011\r\n6029\r\n6037\r\n6043\r\n6047\r\n6053\r\n6067\r\n6073\r\n6079\r\n6089\r\n6091\r\n6101\r\n6113\r\n6121\r\n6131\r\n6133\r\n6143\r\n6151\r\n6163\r\n6173\r\n6197\r\n6199\r\n6203\r\n6211\r\n6217\r\n6221\r\n6229\r\n6247\r\n6257\r\n6263\r\n6269\r\n6271\r\n6277\r\n6287\r\n6299\r\n6301\r\n6311\r\n6317\r\n6323\r\n6329\r\n6337\r\n6343\r\n6353\r\n6359\r\n6361\r\n6367\r\n6373\r\n6379\r\n6389\r\n6397\r\n6421\r\n6427\r\n6449\r\n6451\r\n6469\r\n6473\r\n6481\r\n6491\r\n6521\r\n6529\r\n6547\r\n6551\r\n6553\r\n6563\r\n6569\r\n6571\r\n6577\r\n6581\r\n6599\r\n6607\r\n6619\r\n6637\r\n6653\r\n6659\r\n6661\r\n6673\r\n6679\r\n6689\r\n6691\r\n6701\r\n6703\r\n6709\r\n6719\r\n6733\r\n6737\r\n6761\r\n6763\r\n6779\r\n6781\r\n6791\r\n6793\r\n6803\r\n6823\r\n6827\r\n6829\r\n6833\r\n6841\r\n6857\r\n6863\r\n6869\r\n6871\r\n6883\r\n6899\r\n6907\r\n6911\r\n6917\r\n6947\r\n6949\r\n6959\r\n6961\r\n6967\r\n6971\r\n6977\r\n6983\r\n6991\r\n6997\r\n7001\r\n7013\r\n7019\r\n7027\r\n7039\r\n7043\r\n7057\r\n7069\r\n7079\r\n7103\r\n7109\r\n7121\r\n7127\r\n7129\r\n7151\r\n7159\r\n7177\r\n7187\r\n7193\r\n7207\r\n7211\r\n7213\r\n7219\r\n7229\r\n7237\r\n7243\r\n7247\r\n7253\r\n7283\r\n7297\r\n7307\r\n7309\r\n7321\r\n7331\r\n7333\r\n7349\r\n7351\r\n7369\r\n7393\r\n7411\r\n7417\r\n7433\r\n7451\r\n7457\r\n7459\r\n7477\r\n7481\r\n7487\r\n7489\r\n7499\r\n7507\r\n7517\r\n7523\r\n7529\r\n7537\r\n7541\r\n7547\r\n7549\r\n7559\r\n7561\r\n7573\r\n7577\r\n7583\r\n7589\r\n7591\r\n7603\r\n7607\r\n7621\r\n7639\r\n7643\r\n7649\r\n7669\r\n7673\r\n7681\r\n7687\r\n7691\r\n7699\r\n7703\r\n7717\r\n7723\r\n7727\r\n7741\r\n7753\r\n7757\r\n7759\r\n7789\r\n7793\r\n7817\r\n7823\r\n7829\r\n7841\r\n7853\r\n7867\r\n7873\r\n7877\r\n7879\r\n7883\r\n7901\r\n7907\r\n7919\r\n",
- "base64Encoded": false,
- "contentType": "text/csv"
- }
- },
- "exitCode": 0
- }
- }
-]
\ No newline at end of file
diff --git a/static/1_LF-Details.png b/static/1_LF-Details.png
deleted file mode 100644
index 3a2be57d619cb66e70f76081929e723e874ffcad..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 253009
zcmeFZcT`hb_bwb11rbC=R8T=sEFeXS^di{kk=_yMAVqo&C;|#9sB|eoDIpLLAs}7h
zC`t)P??pb8H+bH2e1G@5|9#&WcZ|CSB7~i_*P3gVXFhYTJVL8nr=ww}L7`A|
z@;9!ip-?ndC={g+^=^3OMaiQQ_>aO_?Yb-~n{<2(exbCytb7@T$_?AQX0i)@-{W{g
z*BOP{SB3nesCK}a!5f0DHFR8bl$FFx9qf2a%p7i;^LW@f!rds8gtUjFiK(r*%gNj3
zme%%?XJ<+(&YrY3lRT>}q|B%6C}(bEeZ$MiT-{4W!_>>xRMhOOv=kkUgohZ6U}x@P
za?-=@j=i&(ha~&fz+&(-@-i>Gl!TL+g_zovYrpS+-z3?sTwEN*czNC3-Fe&vcpRK8
zc`u5Jit_UD^YZg^!yVktp7t ^CfMr+yD`#oXD{$=cDy+QI%LGN8$A2UizKc2`$x
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