task planner works

This commit is contained in:
ValueOn AG 2025-06-21 03:06:00 +02:00
parent b907d068b3
commit f86b3a9e2e
15 changed files with 589 additions and 251 deletions

View file

@ -15,7 +15,7 @@ Connector_AiOpenai_MAX_TOKENS = 2000
# Anthropic configuration
Connector_AiAnthropic_API_URL = https://api.anthropic.com/v1/messages
Connector_AiAnthropic_API_SECRET = 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
Connector_AiAnthropic_MODEL_NAME = claude-3-opus-20240229
Connector_AiAnthropic_MODEL_NAME = claude-3-5-sonnet-20241022
Connector_AiAnthropic_TEMPERATURE = 0.2
Connector_AiAnthropic_MAX_TOKENS = 2000

View file

@ -9,12 +9,11 @@ import uuid
from datetime import datetime
from typing import Dict, Any, List, Optional, Union
import hashlib
import asyncio
from modules.interfaces.interfaceChatAccess import ChatAccess
from modules.interfaces.interfaceChatModel import (
TaskStatus, UserInputRequest, ChatDocument, TaskItem, ChatStat, ChatLog, ChatMessage, ChatWorkflow, TaskAction
TaskStatus, UserInputRequest, ChatDocument, TaskItem, ChatStat, ChatLog, ChatMessage, ChatWorkflow, TaskAction, TaskResult, ActionResult
)
from modules.interfaces.interfaceAppModel import User
@ -169,7 +168,7 @@ class ChatObjects:
logs=[ChatLog(**log) for log in workflow.get("logs", [])],
messages=[ChatMessage(**msg) for msg in workflow.get("messages", [])],
stats=ChatStat(**workflow.get("dataStats", {})) if workflow.get("dataStats") else None,
mandateId=workflow.get("mandateId", self.currentUser.get("mandateId"))
mandateId=workflow.get("mandateId", self.currentUser.mandateId)
)
except Exception as e:
logger.error(f"Error validating workflow data: {str(e)}")
@ -202,7 +201,7 @@ class ChatObjects:
logs=[],
messages=[],
stats=ChatStat(**created.get("dataStats", {})) if created.get("dataStats") else None,
mandateId=created.get("mandateId", self.currentUser.get("mandateId"))
mandateId=created.get("mandateId", self.currentUser.mandateId)
)
def updateWorkflow(self, workflowId: str, workflowData: Dict[str, Any]) -> ChatWorkflow:
@ -264,6 +263,9 @@ class ChatObjects:
def createWorkflowMessage(self, messageData: Dict[str, Any]) -> ChatMessage:
"""Creates a message for a workflow if user has access."""
try:
# Ensure ID is present
if "id" not in messageData or not messageData["id"]:
messageData["id"] = f"msg_{uuid.uuid4()}"
# Check required fields
requiredFields = ["id", "workflowId"]
for field in requiredFields:
@ -299,16 +301,6 @@ class ChatObjects:
# Create message in database
createdMessage = self.db.recordCreate("workflowMessages", messageData)
# Update workflow's messageIds if this is a new message
if createdMessage:
# Get current messageIds or initialize empty list
messageIds = workflow.messageIds if hasattr(workflow, 'messageIds') else []
# Add the new message ID if not already in the list
if createdMessage["id"] not in messageIds:
messageIds.append(createdMessage["id"])
self.updateWorkflow(workflowId, {"messageIds": messageIds})
# Convert to ChatMessage model
return ChatMessage(
id=createdMessage["id"],
@ -319,7 +311,7 @@ class ChatObjects:
message=createdMessage.get("message"),
role=createdMessage.get("role", "assistant"),
status=createdMessage.get("status", "step"),
sequenceNr=len(messageIds), # Set sequence number based on message position
sequenceNr=len(workflow.messages) + 1, # Use messages list length for sequence number
publishedAt=createdMessage.get("publishedAt", self._getCurrentTimestamp()),
stats=ChatStat(**createdMessage.get("stats", {})) if createdMessage.get("stats") else None
)
@ -702,15 +694,6 @@ class ChatObjects:
messageCount = len(messages)
logger.debug(f"Loaded {messageCount} messages for workflow {workflowId}")
# Check if messageIds exists and is valid
messageIds = workflow.get("messageIds", [])
if not messageIds or len(messageIds) != len(messages):
# Rebuild messageIds from messages
messageIds = [msg.get("id") for msg in messages]
# Update in database
self.updateWorkflow(workflowId, {"messageIds": messageIds})
logger.debug(f"Rebuilt messageIds for workflow {workflowId}")
# Log document counts for each message
for msg in messages:
docCount = len(msg.get("documents", []))
@ -725,7 +708,6 @@ class ChatObjects:
# Assemble complete workflow object
completeWorkflow = workflow.copy()
completeWorkflow["messages"] = messages
completeWorkflow["messageIds"] = messageIds
completeWorkflow["logs"] = logs
return completeWorkflow
@ -862,6 +844,7 @@ class ChatObjects:
return TaskItem(
id=task["id"],
workflowId=task["workflowId"],
userInput=task.get("userInput", ""),
status=task.get("status", TaskStatus.PENDING),
error=task.get("error"),
startedAt=task.get("startedAt"),
@ -892,6 +875,9 @@ class ChatObjects:
def createTask(self, taskData: Dict[str, Any]) -> TaskItem:
"""Creates a new task if user has access to the workflow."""
try:
# Ensure ID is present
if "id" not in taskData or not taskData["id"]:
taskData["id"] = f"task_{uuid.uuid4()}"
# Check workflow access
workflowId = taskData.get("workflowId")
if not workflowId:
@ -908,9 +894,6 @@ class ChatObjects:
return None
# Ensure required fields
if "id" not in taskData:
taskData["id"] = f"task_{uuid.uuid4()}"
if "status" not in taskData:
taskData["status"] = TaskStatus.PENDING
@ -924,6 +907,7 @@ class ChatObjects:
task = TaskItem(
id=createdTask["id"],
workflowId=createdTask["workflowId"],
userInput=createdTask.get("userInput", ""),
status=createdTask.get("status", TaskStatus.PENDING),
error=createdTask.get("error"),
startedAt=createdTask.get("startedAt"),
@ -938,7 +922,7 @@ class ChatObjects:
)
# Update workflow's task list
workflowTasks = workflow.get("tasks", [])
workflowTasks = workflow.tasks if hasattr(workflow, 'tasks') else []
if task.id not in workflowTasks:
workflowTasks.append(task.id)
self.updateWorkflow(workflowId, {"tasks": workflowTasks})
@ -975,6 +959,7 @@ class ChatObjects:
return TaskItem(
id=updatedTask["id"],
workflowId=updatedTask["workflowId"],
userInput=updatedTask.get("userInput", task.userInput),
status=updatedTask.get("status", task.status),
error=updatedTask.get("error", task.error),
startedAt=updatedTask.get("startedAt", task.startedAt),
@ -1014,7 +999,7 @@ class ChatObjects:
# Delete task
if self.db.recordDelete("tasks", taskId):
# Update workflow's task list
workflowTasks = workflow.get("tasks", [])
workflowTasks = workflow.tasks if hasattr(workflow, 'tasks') else []
if taskId in workflowTasks:
workflowTasks.remove(taskId)
self.updateWorkflow(task.workflowId, {"tasks": workflowTasks})
@ -1025,6 +1010,128 @@ class ChatObjects:
logger.error(f"Error deleting task: {str(e)}")
return False
# Task Result Management
def createTaskResult(self, resultData: Dict[str, Any]) -> 'TaskResult':
"""Creates a new task result if user has access to the workflow."""
try:
# Ensure ID is present
if "id" not in resultData or not resultData["id"]:
resultData["id"] = f"result_{uuid.uuid4()}"
# Check workflow access if taskId is provided
taskId = resultData.get("taskId")
if taskId:
task = self.getTask(taskId)
if task:
workflow = self.getWorkflow(task.workflowId)
if not workflow:
logger.warning(f"No access to workflow {task.workflowId}")
return None
if not self._canModify("workflows", task.workflowId):
logger.warning(f"No permission to modify workflow {task.workflowId}")
return None
# Ensure required fields
if "status" not in resultData:
resultData["status"] = TaskStatus.PENDING
if "success" not in resultData:
resultData["success"] = False
# Create result in database
createdResult = self.db.recordCreate("taskResults", resultData)
# Convert to TaskResult model
return TaskResult(
taskId=createdResult.get("taskId", ""),
status=createdResult.get("status", TaskStatus.PENDING),
success=createdResult.get("success", False),
feedback=createdResult.get("feedback"),
error=createdResult.get("error")
)
except Exception as e:
logger.error(f"Error creating task result: {str(e)}")
return None
def createActionResult(self, resultData: Dict[str, Any]) -> 'ActionResult':
"""Creates a new action result."""
try:
# Ensure ID is present
if "id" not in resultData or not resultData["id"]:
resultData["id"] = f"action_result_{uuid.uuid4()}"
# Ensure required fields
if "success" not in resultData:
resultData["success"] = False
if "data" not in resultData:
resultData["data"] = {}
# Create result in database
createdResult = self.db.recordCreate("actionResults", resultData)
# Convert to ActionResult model
return ActionResult(
success=createdResult.get("success", False),
data=createdResult.get("data", {}),
metadata=createdResult.get("metadata", {}),
validation=createdResult.get("validation", []),
error=createdResult.get("error")
)
except Exception as e:
logger.error(f"Error creating action result: {str(e)}")
return None
def createTaskAction(self, actionData: Dict[str, Any]) -> TaskAction:
"""Creates a new task action."""
try:
# Ensure ID is present
if "id" not in actionData or not actionData["id"]:
actionData["id"] = f"action_{uuid.uuid4()}"
# Ensure required fields
if "status" not in actionData:
actionData["status"] = TaskStatus.PENDING
if "execMethod" not in actionData:
logger.error("execMethod is required for task action")
return None
if "execAction" not in actionData:
logger.error("execAction is required for task action")
return None
if "execParameters" not in actionData:
actionData["execParameters"] = {}
# Create action in database
createdAction = self.db.recordCreate("taskActions", actionData)
# Convert to TaskAction model
return TaskAction(
id=createdAction["id"],
execMethod=createdAction["execMethod"],
execAction=createdAction["execAction"],
execParameters=createdAction.get("execParameters", {}),
execResultLabel=createdAction.get("execResultLabel"),
status=createdAction.get("status", TaskStatus.PENDING),
error=createdAction.get("error"),
retryCount=createdAction.get("retryCount", 0),
retryMax=createdAction.get("retryMax", 3),
processingTime=createdAction.get("processingTime"),
timestamp=datetime.fromisoformat(createdAction.get("timestamp", datetime.now().isoformat())),
result=createdAction.get("result"),
resultDocuments=createdAction.get("resultDocuments", [])
)
except Exception as e:
logger.error(f"Error creating task action: {str(e)}")
return None
def getInterface(currentUser: Optional[User] = None) -> 'ChatObjects':
"""
Returns a ChatObjects instance for the current user.

View file

@ -2,7 +2,7 @@ from typing import Dict, Any, Optional
import logging
from datetime import datetime, UTC
from modules.methods.methodBase import MethodBase, ActionResult, action
from modules.workflow.methodBase import MethodBase, ActionResult, action
logger = logging.getLogger(__name__)

View file

@ -7,7 +7,7 @@ import logging
from typing import Dict, Any, List, Optional
from modules.workflow.managerDocument import DocumentManager
from modules.methods.methodBase import MethodBase, ActionResult, action
from modules.workflow.methodBase import MethodBase, ActionResult, action
logger = logging.getLogger(__name__)

View file

@ -9,7 +9,7 @@ from datetime import datetime, UTC
import json
import base64
from modules.methods.methodBase import MethodBase, ActionResult, action
from modules.workflow.methodBase import MethodBase, ActionResult, action
logger = logging.getLogger(__name__)
@ -23,14 +23,21 @@ class ExcelService:
"""Get Microsoft connection from connection reference"""
try:
userConnection = self.serviceContainer.getUserConnectionFromConnectionReference(connectionReference)
if userConnection and userConnection.authority == "microsoft" and userConnection.enabled:
return {
"id": userConnection.id,
"accessToken": userConnection.accessToken,
"refreshToken": userConnection.refreshToken,
"scopes": userConnection.scopes
}
return None
if not userConnection or userConnection.authority != "msft" or userConnection.status != "active":
return None
# Get the corresponding token for this user and authority
token = self.serviceContainer.interfaceApp.getToken(userConnection.authority)
if not token:
logger.warning(f"No token found for user {userConnection.userId} and authority {userConnection.authority}")
return None
return {
"id": userConnection.id,
"accessToken": token.tokenAccess,
"refreshToken": token.tokenRefresh,
"scopes": ["Mail.ReadWrite", "User.Read"] # Default Microsoft scopes
}
except Exception as e:
logger.error(f"Error getting Microsoft connection: {str(e)}")
return None

View file

@ -4,7 +4,7 @@ from typing import Dict, List, Any, Optional
from datetime import datetime, UTC
import logging
from modules.methods.methodBase import MethodBase, ActionResult, action
from modules.workflow.methodBase import MethodBase, ActionResult, action
logger = logging.getLogger(__name__)

View file

@ -8,7 +8,7 @@ from typing import Dict, Any, List, Optional
from datetime import datetime, UTC
import json
from modules.methods.methodBase import MethodBase, ActionResult, action
from modules.workflow.methodBase import MethodBase, ActionResult, action
logger = logging.getLogger(__name__)
@ -22,14 +22,21 @@ class OutlookService:
"""Get Microsoft connection from connection reference"""
try:
userConnection = self.serviceContainer.getUserConnectionFromConnectionReference(connectionReference)
if userConnection and userConnection.authority == "microsoft" and userConnection.enabled:
return {
"id": userConnection.id,
"accessToken": userConnection.accessToken,
"refreshToken": userConnection.refreshToken,
"scopes": userConnection.scopes
}
return None
if not userConnection or userConnection.authority != "msft" or userConnection.status != "active":
return None
# Get the corresponding token for this user and authority
token = self.serviceContainer.interfaceApp.getToken(userConnection.authority)
if not token:
logger.warning(f"No token found for user {userConnection.userId} and authority {userConnection.authority}")
return None
return {
"id": userConnection.id,
"accessToken": token.tokenAccess,
"refreshToken": token.tokenRefresh,
"scopes": ["Mail.ReadWrite", "User.Read"] # Default Microsoft scopes
}
except Exception as e:
logger.error(f"Error getting Microsoft connection: {str(e)}")
return None

View file

@ -9,7 +9,7 @@ from datetime import datetime, UTC
import json
import base64
from modules.methods.methodBase import MethodBase, ActionResult, action
from modules.workflow.methodBase import MethodBase, ActionResult, action
logger = logging.getLogger(__name__)
@ -23,14 +23,21 @@ class PowerpointService:
"""Get Microsoft connection from connection reference"""
try:
userConnection = self.serviceContainer.getUserConnectionFromConnectionReference(connectionReference)
if userConnection and userConnection.authority == "microsoft" and userConnection.enabled:
return {
"id": userConnection.id,
"accessToken": userConnection.accessToken,
"refreshToken": userConnection.refreshToken,
"scopes": userConnection.scopes
}
return None
if not userConnection or userConnection.authority != "msft" or userConnection.status != "active":
return None
# Get the corresponding token for this user and authority
token = self.serviceContainer.interfaceApp.getToken(userConnection.authority)
if not token:
logger.warning(f"No token found for user {userConnection.userId} and authority {userConnection.authority}")
return None
return {
"id": userConnection.id,
"accessToken": token.tokenAccess,
"refreshToken": token.tokenRefresh,
"scopes": ["Mail.ReadWrite", "User.Read"] # Default Microsoft scopes
}
except Exception as e:
logger.error(f"Error getting Microsoft connection: {str(e)}")
return None

View file

@ -8,7 +8,7 @@ from typing import Dict, Any, List, Optional
from datetime import datetime, UTC
import json
from modules.methods.methodBase import MethodBase, ActionResult, action
from modules.workflow.methodBase import MethodBase, ActionResult, action
logger = logging.getLogger(__name__)
@ -22,14 +22,21 @@ class SharepointService:
"""Get Microsoft connection from connection reference"""
try:
userConnection = self.serviceContainer.getUserConnectionFromConnectionReference(connectionReference)
if userConnection and userConnection.authority == "microsoft" and userConnection.enabled:
return {
"id": userConnection.id,
"accessToken": userConnection.accessToken,
"refreshToken": userConnection.refreshToken,
"scopes": userConnection.scopes
}
return None
if not userConnection or userConnection.authority != "msft" or userConnection.status != "active":
return None
# Get the corresponding token for this user and authority
token = self.serviceContainer.interfaceApp.getToken(userConnection.authority)
if not token:
logger.warning(f"No token found for user {userConnection.userId} and authority {userConnection.authority}")
return None
return {
"id": userConnection.id,
"accessToken": token.tokenAccess,
"refreshToken": token.tokenRefresh,
"scopes": ["Mail.ReadWrite", "User.Read"] # Default Microsoft scopes
}
except Exception as e:
logger.error(f"Error getting Microsoft connection: {str(e)}")
return None

View file

@ -10,7 +10,7 @@ import requests
from bs4 import BeautifulSoup
import time
from modules.methods.methodBase import MethodBase, ActionResult, action
from modules.workflow.methodBase import MethodBase, ActionResult, action
from modules.shared.configuration import APP_CONFIG
logger = logging.getLogger(__name__)

View file

@ -2,8 +2,6 @@ import logging
from typing import Dict, Any, Optional, List, Union
from datetime import datetime, UTC
import json
import uuid
import time
from modules.interfaces.interfaceAppModel import User
from modules.interfaces.interfaceChatModel import (
@ -29,10 +27,39 @@ class ChatManager:
self.workflow = workflow
self.service = ServiceContainer(self.currentUser, self.workflow)
def _extractJsonFromResponse(self, response: str) -> Optional[Dict[str, Any]]:
"""Extract JSON from verbose AI response that may contain explanatory text"""
try:
# First try direct JSON parsing
return json.loads(response)
except json.JSONDecodeError:
# Try to find JSON in the response
import re
# Look for JSON object patterns
json_patterns = [
r'\{.*\}', # Basic JSON object
r'\[\{.*\}\]', # JSON array of objects
]
for pattern in json_patterns:
matches = re.findall(pattern, response, re.DOTALL)
for match in matches:
try:
return json.loads(match)
except json.JSONDecodeError:
continue
# If no JSON found, log the full response for debugging
logger.error(f"Could not extract JSON from response: {response[:500]}...")
return None
# ===== Task Creation and Management =====
async def createInitialTask(self, workflow: ChatWorkflow, initialMessage: ChatMessage) -> Optional[TaskItem]:
"""Create the initial task from the first message"""
try:
logger.info(f"Creating initial task for workflow {workflow.id}")
# Create task definition prompt
prompt = await self._createTaskDefinitionPrompt(initialMessage.message, workflow)
@ -40,13 +67,13 @@ class ChatManager:
response = await self.service.callAiTextAdvanced(prompt)
# Parse response
try:
taskDef = json.loads(response)
except json.JSONDecodeError:
logger.error(f"Invalid JSON in task definition: {response}")
return None
taskDef = self._extractJsonFromResponse(response)
# Validate task definition
if not taskDef:
logger.error("Could not extract valid JSON from AI response")
return None
if not isinstance(taskDef, dict):
logger.error("Task definition must be a JSON object")
return None
@ -61,6 +88,8 @@ class ChatManager:
logger.error("Actions must be a list")
return None
logger.info(f"Task definition validated: {len(taskDef['actions'])} actions")
# Create task using interface
taskData = {
"workflowId": workflow.id,
@ -79,17 +108,40 @@ class ChatManager:
if not all(field in actionDef for field in requiredFields):
continue
action = TaskAction(
id=str(uuid.uuid4()),
execMethod=actionDef["method"],
execAction=actionDef["action"],
execParameters=actionDef["parameters"],
execResultLabel=actionDef.get("resultLabel")
)
taskData["actionList"].append(action)
# Create action using interface
actionData = {
"execMethod": actionDef["method"],
"execAction": actionDef["action"],
"execParameters": actionDef["parameters"],
"execResultLabel": actionDef.get("resultLabel")
}
action = self.chatInterface.createTaskAction(actionData)
if action:
# Convert TaskAction object to dictionary for database storage
actionDict = {
"id": action.id,
"execMethod": action.execMethod,
"execAction": action.execAction,
"execParameters": action.execParameters,
"execResultLabel": action.execResultLabel,
"status": action.status,
"error": action.error,
"retryCount": action.retryCount,
"retryMax": action.retryMax,
"processingTime": action.processingTime,
"timestamp": action.timestamp.isoformat() if action.timestamp else None,
"result": action.result,
"resultDocuments": action.resultDocuments
}
taskData["actionList"].append(actionDict)
# Create task using interface
task = self.chatInterface.createTask(taskData)
if task:
logger.info(f"Task created successfully: {task.id}")
else:
logger.error("Failed to create task")
return task
except Exception as e:
@ -99,6 +151,8 @@ class ChatManager:
async def createNextTask(self, workflow: ChatWorkflow, previousResult: TaskResult) -> Optional[TaskItem]:
"""Create next task based on previous result"""
try:
logger.info(f"Creating next task for workflow {workflow.id}")
# Check if previous result was successful
if not previousResult.success:
logger.error(f"Previous task failed: {previousResult.error}")
@ -111,13 +165,13 @@ class ChatManager:
response = await self.service.callAiTextAdvanced(prompt)
# Parse response
try:
taskDef = json.loads(response)
except json.JSONDecodeError:
logger.error(f"Invalid JSON in task definition: {response}")
return None
taskDef = self._extractJsonFromResponse(response)
# Validate task definition
if not taskDef:
logger.error("Could not extract valid JSON from AI response")
return None
if not isinstance(taskDef, dict):
logger.error("Task definition must be a JSON object")
return None
@ -132,6 +186,8 @@ class ChatManager:
logger.error("Actions must be a list")
return None
logger.info(f"Next task definition validated: {len(taskDef['actions'])} actions")
# Create task using interface
taskData = {
"workflowId": workflow.id,
@ -150,17 +206,40 @@ class ChatManager:
if not all(field in actionDef for field in requiredFields):
continue
action = TaskAction(
id=str(uuid.uuid4()),
execMethod=actionDef["method"],
execAction=actionDef["action"],
execParameters=actionDef["parameters"],
execResultLabel=actionDef.get("resultLabel")
)
taskData["actionList"].append(action)
# Create action using interface
actionData = {
"execMethod": actionDef["method"],
"execAction": actionDef["action"],
"execParameters": actionDef["parameters"],
"execResultLabel": actionDef.get("resultLabel")
}
action = self.chatInterface.createTaskAction(actionData)
if action:
# Convert TaskAction object to dictionary for database storage
actionDict = {
"id": action.id,
"execMethod": action.execMethod,
"execAction": action.execAction,
"execParameters": action.execParameters,
"execResultLabel": action.execResultLabel,
"status": action.status,
"error": action.error,
"retryCount": action.retryCount,
"retryMax": action.retryMax,
"processingTime": action.processingTime,
"timestamp": action.timestamp.isoformat() if action.timestamp else None,
"result": action.result,
"resultDocuments": action.resultDocuments
}
taskData["actionList"].append(actionDict)
# Create task using interface
task = self.chatInterface.createTask(taskData)
if task:
logger.info(f"Next task created successfully: {task.id}")
else:
logger.error("Failed to create next task")
return task
except Exception as e:
@ -198,9 +277,8 @@ Example format:
response = await self.service.callAiTextBasic(prompt)
# Parse response
try:
result = json.loads(response)
except json.JSONDecodeError:
result = self._extractJsonFromResponse(response)
if not result:
logger.error(f"Invalid JSON in action result: {response}")
action.status = "failed"
action.error = "Invalid result format"
@ -229,6 +307,8 @@ Example format:
if message:
self.workflow.messages.append(message)
logger.info(f"Action execution logged: {action.execMethod}.{action.execAction} - {action.status}")
# If action failed, stop execution
if action.status == "failed":
break
@ -324,33 +404,35 @@ Example format:
response = await self.service.callAiTextBasic(prompt)
# Parse response
try:
result = json.loads(response)
except json.JSONDecodeError:
result = self._extractJsonFromResponse(response)
if not result:
logger.error(f"Invalid JSON in next task identification: {response}")
return TaskResult(
taskId=str(uuid.uuid4()),
status="failed",
success=False,
error="Invalid result format"
)
# Create error result using interface
errorResultData = {
"status": "failed",
"success": False,
"error": "Invalid result format"
}
return self.chatInterface.createTaskResult(errorResultData)
return TaskResult(
taskId=str(uuid.uuid4()),
status="completed" if result.get("success", False) else "failed",
success=result.get("success", False),
feedback=result.get("feedback", ""),
error=result.get("error", "")
)
# Create result using interface
resultData = {
"status": "completed" if result.get("success", False) else "failed",
"success": result.get("success", False),
"feedback": result.get("feedback", ""),
"error": result.get("error", "")
}
return self.chatInterface.createTaskResult(resultData)
except Exception as e:
logger.error(f"Error identifying next task: {str(e)}")
return TaskResult(
taskId=str(uuid.uuid4()),
status="failed",
success=False,
error=str(e)
)
# Create error result using interface
errorResultData = {
"status": "failed",
"success": False,
"error": str(e)
}
return self.chatInterface.createTaskResult(errorResultData)
async def generateWorkflowFeedback(self, workflow: ChatWorkflow) -> str:
"""Generate final feedback for the workflow"""
@ -408,79 +490,93 @@ Please provide a comprehensive summary of this workflow."""
docRefs = self.service.getDocumentReferenceList()
connRefs = self.service.getConnectionReferenceList()
return f"""
Task Definition for: {userInput}
prompt = f"""You are a task planning AI that creates structured task definitions in JSON format.
Chat History:
{messageSummary}
TASK REQUEST: {userInput}
Available Methods:
{chr(10).join(f"- {method}" for method in methodList)}
CONTEXT:
Chat History: {messageSummary}
Available Documents:
{chr(10).join(f"- {doc['documentReference']} ({doc['datetime']})" for doc in docRefs.get('chat', []))}
AVAILABLE RESOURCES:
Methods: {chr(10).join(f"- {method}" for method in methodList)}
Available Connections:
{chr(10).join(f"- {conn['connectionReference']} ({conn['authority']})" for conn in connRefs)}
Documents: {chr(10).join(f"- {doc['documentReference']} ({doc['datetime']})" for doc in docRefs.get('chat', []))}
Your Task:
1. Analyze the user input and chat history
2. Determine what actions are needed to accomplish the task
3. Create a sequence of actions using only the available methods, documents, and connections
4. Provide feedback about what will be done and what needs to be done next
Connections: {chr(10).join(f"- {conn['connectionReference']} ({conn['authority']})" for conn in connRefs)}
Required Output:
1. A JSON object containing:
- status: Current state of the task ("pending", "running", "completed", or "failed")
- feedback: Explanation of what will be done and what needs to be done next
- actions: List of actions to execute, each containing:
* method: The method to use
* action: The specific action to perform
* parameters: Required parameters for the action
* resultLabel: Label for the action's result
INSTRUCTIONS:
1. Analyze the task request and available resources
2. Create a sequence of actions to accomplish the task
3. Use ONLY the provided methods, documents, and connections
4. Return a VALID JSON object with the exact structure shown below
2. Available Data:
- Use only provided document references in Available Documents section
- Use only provided connection references in Available Connections section
3. Method Usage Rules:
- Syntax: method.action([parameter:type])->resultLabel:type
- resultLabel format: documentList_<uuid>_<label>
- Actions must be in processing sequence
- Parameters must be from:
* Available document references
* Available connection references
* Result labels from previous actions
4. Result Labels:
- Use consistent naming for related documents
- Include descriptive labels for document sets
- Labels will be used to track document sets in messages
5. Error Handling:
- Include validation for each action
- Specify retry behavior if needed
- Provide clear error messages
- Errors will be recorded in messages with .error: suffix
Example Format:
REQUIRED JSON STRUCTURE:
{{
"status": "pending",
"feedback": "Will search for documents about project X and analyze them",
"feedback": "Clear explanation of what will be done",
"actions": [
{{
"method": "method_name",
"action": "action_name",
"parameters": {{
"param1": "value1",
"param2": "value2"
}},
"resultLabel": "documentList_uuid_label"
}}
]
}}
JSON FIELD REQUIREMENTS:
- "status": Must be "pending", "running", "completed", or "failed"
- "feedback": Human-readable explanation of the task plan
- "actions": Array of action objects (can be empty if no actions needed)
- "method": Must be one of the available methods listed above
- "action": Must be a valid action for that method
- "parameters": Object with method-specific parameters
- "resultLabel": Format: "documentList_uuid_descriptive_label"
PARAMETER RULES:
- Use only document references from "Documents" section above
- Use only connection references from "Connections" section above
- Use result labels from previous actions in the sequence
- All parameter values must be strings
EXAMPLE VALID JSON:
{{
"status": "pending",
"feedback": "I will search SharePoint for sales documents and then analyze the quarterly data to create a business intelligence report.",
"actions": [
{{
"method": "sharepoint",
"action": "search",
"parameters": {{
"query": "project X",
"site": "projects"
"query": "sales quarterly report",
"site": "connection_123_msft_testuser@example.com"
}},
"resultLabel": "documentList_<uuid>_search_results"
"resultLabel": "documentList_abc123_sales_documents"
}},
{{
"method": "excel",
"action": "analyze",
"parameters": {{
"document": "documentList_abc123_sales_documents"
}},
"resultLabel": "documentList_def456_analysis_results"
}}
]
}}
Please provide the task definition in JSON format following these rules."""
CRITICAL: Respond with ONLY the JSON object. Do not include any explanatory text, markdown formatting, or additional content outside the JSON structure."""
# Log the generated prompt for debugging
logger.debug("=" * 80)
logger.debug("TASK DEFINITION PROMPT:")
logger.debug("=" * 80)
logger.debug(prompt)
logger.debug("=" * 80)
return prompt
# ===== Utility Methods =====
async def processFileIds(self, fileIds: List[str]) -> List[ChatDocument]:
@ -491,14 +587,16 @@ Please provide the task definition in JSON format following these rules."""
# Get file info from service
fileInfo = self.service.getFileInfo(fileId)
if fileInfo:
document = ChatDocument(
id=str(uuid.uuid4()),
fileId=fileId,
filename=fileInfo.get("filename", "unknown"),
fileSize=fileInfo.get("size", 0),
mimeType=fileInfo.get("mimeType", "application/octet-stream")
)
documents.append(document)
# Create document using interface
documentData = {
"fileId": fileId,
"filename": fileInfo.get("filename", "unknown"),
"fileSize": fileInfo.get("size", 0),
"mimeType": fileInfo.get("mimeType", "application/octet-stream")
}
document = self.chatInterface.createChatDocument(documentData)
if document:
documents.append(document)
except Exception as e:
logger.error(f"Error processing file ID {fileId}: {str(e)}")
return documents

View file

@ -1,7 +1,6 @@
from typing import Dict, Any
import logging
from datetime import datetime, UTC
import uuid
from modules.interfaces.interfaceAppObjects import User
@ -31,6 +30,8 @@ class WorkflowManager:
async def workflowProcess(self, userInput: UserInputRequest, workflow: ChatWorkflow) -> TaskItem:
"""Process a workflow with user input"""
logger.info(f"Processing workflow: {workflow.id}")
# Initialize chat manager
await self.chatManager.initialize(workflow)

View file

@ -12,8 +12,9 @@ from modules.interfaces.interfaceAiCalls import AiCalls
from modules.interfaces.interfaceChatObjects import getInterface as getChatObjects
from modules.interfaces.interfaceChatModel import ActionResult
from modules.interfaces.interfaceComponentObjects import getInterface as getComponentObjects
from modules.interfaces.interfaceAppObjects import getInterface as getAppObjects
from modules.workflow.managerDocument import DocumentManager
from modules.methods.methodBase import MethodBase
from modules.workflow.methodBase import MethodBase
import uuid
import base64
@ -33,6 +34,7 @@ class ServiceContainer:
# Initialize managers
self.interfaceChat = getChatObjects(currentUser)
self.interfaceComponent = getComponentObjects(currentUser)
self.interfaceApp = getAppObjects(currentUser)
self.interfaceAiCalls = AiCalls()
self.documentManager = DocumentManager(self)
@ -64,14 +66,12 @@ class ServiceContainer:
# Discover actions from public methods
actions = {}
for methodName, method in inspect.getmembers(methodInstance, predicate=inspect.isfunction):
# Skip private methods and inherited methods
if not methodName.startswith('_') and methodName not in ['execute', 'actions', 'validateParameters']:
# Get method signature
for methodName, method in inspect.getmembers(type(methodInstance), predicate=inspect.iscoroutinefunction):
if not methodName.startswith('_') and methodName not in ['execute', 'validateParameters']:
# Bind the method to the instance
bound_method = method.__get__(methodInstance, type(methodInstance))
sig = inspect.signature(method)
params = {}
# Convert parameters to action definition
for paramName, param in sig.parameters.items():
if paramName not in ['self', 'authData']:
params[paramName] = {
@ -79,12 +79,10 @@ class ServiceContainer:
'required': param.default == param.empty,
'description': param.default.__doc__ if hasattr(param.default, '__doc__') else None
}
# Add action definition
actions[methodName] = {
'description': method.__doc__ or '',
'parameters': params,
'method': method
'method': bound_method
}
# Add method instance with discovered actions
@ -96,7 +94,7 @@ class ServiceContainer:
logger.info(f"Discovered method: {methodInstance.name} with {len(actions)} actions")
except Exception as e:
logger.error(f"Error loading method module {name}: {str(e)}")
logger.error(f"Error loading method module {name}: {str(e)}", exc_info=True)
except Exception as e:
logger.error(f"Error discovering methods: {str(e)}")
@ -255,9 +253,11 @@ class ServiceContainer:
def getConnectionReferenceList(self) -> List[Dict[str, str]]:
"""Get list of all UserConnection objects as references"""
connections = []
for conn in self.user.connections:
# Get user connections through AppObjects interface
user_connections = self.interfaceApp.getUserConnections(self.user.id)
for conn in user_connections:
connections.append({
"connectionReference": f"connection_{conn.id}_{conn.authority}",
"connectionReference": f"connection_{conn.id}_{conn.authority}_{conn.externalUsername}",
"authority": conn.authority
})
# Sort by authority
@ -265,22 +265,26 @@ class ServiceContainer:
def getConnectionReferenceFromUserConnection(self, connection: UserConnection) -> str:
"""Get connection reference from UserConnection"""
return f"connection_{connection.id}_{connection.authority}"
return f"connection_{connection.id}_{connection.authority}_{connection.externalUsername}"
def getUserConnectionFromConnectionReference(self, connectionReference: str) -> Optional[UserConnection]:
"""Get UserConnection from reference string"""
try:
# Parse reference format: connection_{id}_{authority}
# Parse reference format: connection_{id}_{authority}_{username}
parts = connectionReference.split('_')
if len(parts) != 3 or parts[0] != "connection":
if len(parts) != 4 or parts[0] != "connection":
return None
conn_id = parts[1]
authority = parts[2]
username = parts[3]
# Get user connections through AppObjects interface
user_connections = self.interfaceApp.getUserConnections(self.user.id)
# Find matching connection
for conn in self.user.connections:
if str(conn.id) == conn_id and conn.authority == authority:
for conn in user_connections:
if str(conn.id) == conn_id and conn.authority == authority and conn.externalUsername == username:
return conn
return None
@ -379,7 +383,17 @@ Please provide a clear summary of this message."""
def getFileInfo(self, fileId: str) -> Dict[str, Any]:
"""Get file information"""
return self.interfaceComponent.getFileInfo(fileId)
file_item = self.interfaceComponent.getFile(fileId)
if file_item:
return {
"id": file_item.id,
"filename": file_item.filename,
"size": file_item.fileSize,
"mimeType": file_item.mimeType,
"fileHash": file_item.fileHash,
"creationDate": file_item.creationDate
}
return None
def getFileData(self, fileId: str) -> bytes:
"""Get file data by ID"""

View file

@ -7,46 +7,47 @@ import asyncio
import logging
import sys
import os
from datetime import datetime, UTC
import json
from datetime import datetime, UTC, timedelta
import uuid
# Set up test configuration
os.environ['POWERON_CONFIG_FILE'] = 'test_config.ini'
print("Starting test_workflow.py...")
# Simple imports from modules (same as app.py)
from modules.interfaces.interfaceAppObjects import User, UserConnection
from modules.interfaces.interfaceChatObjects import ChatObjects
from modules.interfaces.interfaceChatModel import UserInputRequest, ChatWorkflow
from modules.workflow.managerWorkflow import WorkflowManager
# Configure logging
# Configure logging FIRST, before any other imports
logging.basicConfig(
level=logging.INFO,
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('test_workflow.log')
]
logging.FileHandler('test_workflow.log', encoding='utf-8')
],
force=True # Force reconfiguration even if already configured
)
logger = logging.getLogger(__name__)
print("Logger level:", logger.level)
logger.info("Logger is working!")
print("Logger test done")
# Set up test configuration
os.environ['POWERON_CONFIG_FILE'] = 'test_config.ini'
print("Set POWERON_CONFIG_FILE environment variable")
try:
# Simple imports from modules (same as app.py)
from modules.interfaces.interfaceAppObjects import User, UserConnection
from modules.interfaces.interfaceChatObjects import ChatObjects
from modules.interfaces.interfaceChatModel import UserInputRequest, ChatWorkflow
from modules.workflow.managerWorkflow import WorkflowManager
print("All imports successful")
except Exception as e:
print(f"Import error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
def create_test_user() -> User:
"""Create a test user for the workflow"""
# Create test connections for Microsoft services
connections = [
UserConnection(
id="conn-001",
authority="microsoft",
name="Test Microsoft Account",
enabled=True,
accessToken="test-token-123",
refreshToken="test-refresh-456",
expiresAt=datetime.now(UTC).isoformat(),
scopes=["Files.ReadWrite", "Mail.ReadWrite", "Sites.ReadWrite.All"]
)
]
return User(
id="test-user-001",
mandateId="test-mandate-001",
@ -56,8 +57,7 @@ def create_test_user() -> User:
enabled=True,
language="en",
privilege="user",
authenticationAuthority="local",
connections=connections
authenticationAuthority="local"
)
def create_test_workflow() -> ChatWorkflow:
@ -103,6 +103,7 @@ def create_test_user_input() -> UserInputRequest:
)
async def test_workflow_process():
print("Inside test_workflow_process()")
"""Test the workflowProcess function"""
try:
logger.info("Starting workflow process test...")
@ -112,17 +113,86 @@ async def test_workflow_process():
test_workflow = create_test_workflow()
test_user_input = create_test_user_input()
logger.info(f"Test user: {test_user.username}")
logger.info(f"Test workflow: {test_workflow.id}")
logger.info(f"Test input prompt: {test_user_input.prompt[:100]}...")
logger.info(f"Test files: {test_user_input.listFileId}")
# Create test user in database through AppObjects interface
from modules.interfaces.interfaceAppObjects import getRootInterface
from modules.interfaces.interfaceAppModel import AuthAuthority, ConnectionStatus, Token, UserPrivilege
# Initialize ChatObjects interface
chat_interface = ChatObjects(test_user)
logger.info("ChatObjects interface initialized")
root_interface = getRootInterface()
created_user = root_interface.createUser(
username=test_user.username,
password="testpassword123", # Required for local authentication
email=test_user.email,
fullName=test_user.fullName,
language=test_user.language,
enabled=test_user.enabled,
privilege=UserPrivilege.USER,
authenticationAuthority=AuthAuthority.LOCAL
)
logger.info(f"Created test user in database: {created_user.id}")
# Create test connection through AppObjects interface
from modules.interfaces.interfaceAppObjects import getInterface as getAppObjects
app_interface = getAppObjects(created_user)
test_connection = app_interface.addUserConnection(
userId=created_user.id,
authority=AuthAuthority.MSFT,
externalId="msft-user-123",
externalUsername="testuser@example.com",
externalEmail="testuser@example.com",
status=ConnectionStatus.ACTIVE
)
logger.info(f"Created test connection: {test_connection.id}")
# Create test token for the connection
test_token = Token(
userId=created_user.id,
authority=AuthAuthority.MSFT,
tokenAccess="test-access-token-123",
tokenRefresh="test-refresh-token-456",
tokenType="bearer",
expiresAt=datetime.now(UTC).timestamp() + 3600, # 1 hour from now
createdAt=datetime.now(UTC)
)
app_interface.saveToken(test_token)
logger.info(f"Created test token for connection: {test_token.id}")
logger.info(f"Test user: {created_user.username}")
logger.info(f"Test workflow: {test_workflow.id}")
# Log the full prompt in JSON format
logger.debug("=" * 60)
logger.debug("USER INPUT PROMPT (JSON):")
logger.debug("=" * 60)
prompt_data = {
"prompt": test_user_input.prompt,
"listFileId": test_user_input.listFileId,
"userLanguage": test_user_input.userLanguage
}
logger.debug(json.dumps(prompt_data, indent=2, ensure_ascii=False))
logger.debug("=" * 60)
logger.debug(f"Test files: {test_user_input.listFileId}")
# Create test workflow in database through ChatObjects interface
from modules.interfaces.interfaceChatObjects import getInterface as getChatObjects
chat_interface = getChatObjects(created_user)
workflow_data = {
"name": test_workflow.name,
"status": test_workflow.status,
"mandateId": created_user.mandateId,
"currentRound": test_workflow.currentRound,
"startedAt": test_workflow.startedAt,
"lastActivity": test_workflow.lastActivity
}
created_workflow = chat_interface.createWorkflow(workflow_data)
logger.info(f"Created test workflow: {created_workflow.id}")
# Update the test_workflow object with the created workflow's ID
test_workflow.id = created_workflow.id
# Initialize WorkflowManager
workflow_manager = WorkflowManager(chat_interface, test_user)
workflow_manager = WorkflowManager(chat_interface, created_user)
logger.info("WorkflowManager initialized")
# Test the workflowProcess function
@ -131,18 +201,37 @@ async def test_workflow_process():
# Log results
if task:
logger.info("Task created successfully!")
logger.info(f"Task ID: {task.id}")
logger.info(f"Task Status: {task.status}")
logger.info(f"Task Feedback: {task.feedback}")
logger.debug("Task created successfully!")
logger.debug(f"Task ID: {task.id}")
logger.debug(f"Task Status: {task.status}")
logger.debug(f"Task Feedback: {task.feedback}")
logger.info(f"Number of actions: {len(task.actionList) if task.actionList else 0}")
# Log the full task object in JSON format
logger.debug("=" * 60)
logger.debug("TASK OBJECT (JSON):")
logger.debug("=" * 60)
task_data = {
"id": task.id,
"status": task.status,
"feedback": task.feedback,
"actionList": [
{
"execMethod": action.execMethod,
"execAction": action.execAction,
"execParameters": action.execParameters
} for action in (task.actionList or [])
] if task.actionList else []
}
logger.debug(json.dumps(task_data, indent=2, ensure_ascii=False))
logger.debug("=" * 60)
if task.actionList:
for i, action in enumerate(task.actionList):
logger.info(f"Action {i+1}: {action.execMethod}.{action.execAction}")
logger.info(f" Parameters: {action.execParameters}")
else:
logger.warning("⚠️ No task was created")
logger.warning("No task was created")
logger.info("Test completed successfully!")
return task
@ -153,7 +242,7 @@ async def test_workflow_process():
raise
async def main():
"""Main function to run the test"""
print("Inside main()")
logger.info("=" * 50)
logger.info("BUSINESS INTELLIGENCE WORKFLOW TEST")
logger.info("=" * 50)
@ -171,5 +260,6 @@ async def main():
raise
if __name__ == "__main__":
# Run the test
asyncio.run(main())
print("About to run main()")
asyncio.run(main())
print("main() finished")