CHAT 2.0 - Iterative mode

This commit is contained in:
ValueOn AG 2025-09-23 00:36:24 +02:00
parent 30d0a8f70c
commit 1019cb7a65
9 changed files with 292 additions and 25 deletions

3
app.py
View file

@ -309,6 +309,9 @@ app.include_router(connectionsRouter)
from modules.routes.routeWorkflows import router as workflowRouter
app.include_router(workflowRouter)
from modules.routes.routeChatPlayground import router as chatPlaygroundRouter
app.include_router(chatPlaygroundRouter)
from modules.routes.routeSecurityLocal import router as localRouter
app.include_router(localRouter)

View file

@ -8,12 +8,24 @@ from modules.shared.timezoneUtils import get_utc_timestamp
logger = logging.getLogger(__name__)
async def chatStart(interfaceChat, currentUser: User, userInput: UserInputRequest, workflowId: Optional[str] = None) -> ChatWorkflow:
"""Starts a new chat or continues an existing one, then launches processing asynchronously."""
async def chatStart(interfaceChat, currentUser: User, userInput: UserInputRequest, workflowId: Optional[str] = None, workflowMode: str = "Actionplan") -> ChatWorkflow:
"""
Starts a new chat or continues an existing one, then launches processing asynchronously.
Args:
interfaceChat: Chat interface instance
currentUser: Current user
userInput: User input request
workflowId: Optional workflow ID to continue existing workflow
workflowMode: "Actionplan" for traditional task planning, "React" for iterative react-style processing
Example usage for React mode:
workflow = await chatStart(interfaceChat, currentUser, userInput, workflowMode="React")
"""
try:
from modules.workflows.workflowManager import WorkflowManager
workflowManager = WorkflowManager(interfaceChat, currentUser)
return await workflowManager.workflowStart(userInput, workflowId)
return await workflowManager.workflowStart(userInput, workflowId, workflowMode)
except Exception as e:
logger.error(f"Error starting chat: {str(e)}")
raise

View file

@ -270,7 +270,9 @@ class ChatObjects:
logs=[],
messages=[],
stats=None,
mandateId=created.get("mandateId", self.currentUser.mandateId)
mandateId=created.get("mandateId", self.currentUser.mandateId),
workflowMode=created.get("workflowMode", "Actionplan"),
maxSteps=created.get("maxSteps", 1)
)
def updateWorkflow(self, workflowId: str, workflowData: Dict[str, Any]) -> ChatWorkflow:
@ -885,6 +887,58 @@ class ChatObjects:
stats.sort(key=lambda x: x.get("created_at", ""), reverse=True)
return ChatStat(**stats[0])
def updateWorkflowStats(self, workflowId: str, bytesSent: int = 0, bytesReceived: int = 0, tokenCount: int = 0) -> None:
"""
Updates workflow statistics in the database.
Args:
workflowId: ID of the workflow to update
bytesSent: Bytes sent (incremental)
bytesReceived: Bytes received (incremental)
tokenCount: Token count (incremental, default 0)
"""
try:
# Check workflow access first
workflow = self.getWorkflow(workflowId)
if not workflow:
logger.warning(f"No access to workflow {workflowId} for stats update")
return
if not self._canModify(ChatWorkflow, workflowId):
logger.warning(f"No permission to modify workflow {workflowId} for stats update")
return
# Get existing stats or create new ones
existing_stats = self.getWorkflowStats(workflowId)
if existing_stats:
# Update existing stats
updated_stats = {
"bytesSent": (existing_stats.bytesSent or 0) + bytesSent,
"bytesReceived": (existing_stats.bytesReceived or 0) + bytesReceived,
"tokenCount": (existing_stats.tokenCount or 0) + tokenCount,
"lastUpdated": get_utc_timestamp()
}
# Update the stats record
self.db.recordModify(ChatStat, existing_stats.id, updated_stats)
else:
# Create new stats record
new_stats = {
"workflowId": workflowId,
"bytesSent": bytesSent,
"bytesReceived": bytesReceived,
"tokenCount": tokenCount,
"lastUpdated": get_utc_timestamp()
}
self.db.recordCreate(ChatStat, new_stats)
logger.debug(f"Updated workflow stats for {workflowId}: +{bytesSent} sent, +{bytesReceived} received, +{tokenCount} tokens")
except Exception as e:
logger.error(f"Error updating workflow stats for {workflowId}: {str(e)}")
def getUnifiedChatData(self, workflowId: str, afterTimestamp: Optional[float] = None) -> Dict[str, Any]:
"""
Returns unified chat data (messages, logs, stats) for a workflow in chronological order.

View file

@ -44,19 +44,23 @@ def getServiceChat(currentUser: User):
async def start_workflow(
request: Request,
workflowId: Optional[str] = Query(None, description="Optional ID of the workflow to continue"),
workflowMode: str = Query("Actionplan", description="Workflow mode: 'Actionplan' or 'React'"),
userInput: UserInputRequest = Body(...),
currentUser: User = Depends(getCurrentUser)
) -> ChatWorkflow:
"""
Starts a new workflow or continues an existing one.
Corresponds to State 1 in the state machine documentation.
Args:
workflowMode: "Actionplan" for traditional task planning, "React" for iterative react-style processing
"""
try:
# Get service center
interfaceChat = getServiceChat(currentUser)
# Start or continue workflow using playground controller
workflow = await chatStart(interfaceChat, currentUser, userInput, workflowId)
workflow = await chatStart(interfaceChat, currentUser, userInput, workflowId, workflowMode)
return workflow

View file

@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/api/admin",
tags=["Admin"],
tags=["Security Administration"],
responses={
404: {"description": "Not found"},
400: {"description": "Bad request"},
@ -248,9 +248,145 @@ async def list_databases(
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
_ensure_admin_scope(currentUser)
# For safety, expose only configured database name
db_name = APP_CONFIG.get("DB_DATABASE") or APP_CONFIG.get("DB_NAME") or "poweron"
return {"databases": [db_name]}
# Get database names from configuration for each interface
databases = []
# App database (interfaceAppObjects.py)
app_db = APP_CONFIG.get("DB_APP_DATABASE")
if app_db:
databases.append(app_db)
# Chat database (interfaceChatObjects.py)
chat_db = APP_CONFIG.get("DB_CHAT_DATABASE")
if chat_db:
databases.append(chat_db)
# Management database (interfaceComponentObjects.py)
management_db = APP_CONFIG.get("DB_MANAGEMENT_DATABASE")
if management_db:
databases.append(management_db)
# Fallback to default if no databases configured
if not databases:
databases = ["poweron"]
return {"databases": databases}
@router.get("/databases/{database_name}/tables")
@limiter.limit("30/minute")
async def get_database_tables(
request: Request,
database_name: str,
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
_ensure_admin_scope(currentUser)
# Get all configured database names
configured_dbs = []
app_db = APP_CONFIG.get("DB_APP_DATABASE")
if app_db:
configured_dbs.append(app_db)
chat_db = APP_CONFIG.get("DB_CHAT_DATABASE")
if chat_db:
configured_dbs.append(chat_db)
management_db = APP_CONFIG.get("DB_MANAGEMENT_DATABASE")
if management_db:
configured_dbs.append(management_db)
if not configured_dbs:
configured_dbs = ["poweron"]
if database_name not in configured_dbs:
raise HTTPException(status_code=400, detail=f"Invalid database name. Available databases: {configured_dbs}")
try:
# Use the appropriate interface based on database name
if database_name == app_db:
appInterface = getRootInterface()
tables = appInterface.db.getTables()
elif database_name == chat_db:
from modules.interfaces.interfaceChatObjects import getInterface as getChatInterface
chatInterface = getChatInterface(currentUser)
tables = chatInterface.db.getTables()
elif database_name == management_db:
from modules.interfaces.interfaceComponentObjects import getInterface as getComponentInterface
componentInterface = getComponentInterface(currentUser)
tables = componentInterface.db.getTables()
else:
raise HTTPException(status_code=400, detail="Database not found")
return {"tables": tables}
except Exception as e:
logger.error(f"Error getting database tables: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to get database tables")
@router.post("/databases/{database_name}/tables/{table_name}/drop")
@limiter.limit("10/minute")
async def drop_table(
request: Request,
database_name: str,
table_name: str,
currentUser: User = Depends(getCurrentUser),
payload: Dict[str, Any] = Body(...)
) -> Dict[str, Any]:
_ensure_admin_scope(currentUser)
# Get all configured database names
configured_dbs = []
app_db = APP_CONFIG.get("DB_APP_DATABASE")
if app_db:
configured_dbs.append(app_db)
chat_db = APP_CONFIG.get("DB_CHAT_DATABASE")
if chat_db:
configured_dbs.append(chat_db)
management_db = APP_CONFIG.get("DB_MANAGEMENT_DATABASE")
if management_db:
configured_dbs.append(management_db)
if not configured_dbs:
configured_dbs = ["poweron"]
if database_name not in configured_dbs:
raise HTTPException(status_code=400, detail=f"Invalid database name. Available databases: {configured_dbs}")
try:
# Use the appropriate interface based on database name
if database_name == app_db:
interface = getRootInterface()
elif database_name == chat_db:
from modules.interfaces.interfaceChatObjects import getInterface as getChatInterface
interface = getChatInterface(currentUser)
elif database_name == management_db:
from modules.interfaces.interfaceComponentObjects import getInterface as getComponentInterface
interface = getComponentInterface(currentUser)
else:
raise HTTPException(status_code=400, detail="Database not found")
conn = interface.db.connection
with conn.cursor() as cursor:
# Check if table exists
cursor.execute("""
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = %s
""", (table_name,))
if not cursor.fetchone():
raise HTTPException(status_code=404, detail="Table not found")
# Drop the table
cursor.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE')
conn.commit()
logger.warning(f"Admin drop_table executed by {currentUser.id}: dropped table '{table_name}' from database '{database_name}'")
return {"message": f"Table '{table_name}' dropped successfully from database '{database_name}'"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error dropping table: {str(e)}")
if 'interface' in locals() and interface and interface.db and interface.db.connection:
interface.db.connection.rollback()
raise HTTPException(status_code=500, detail="Failed to drop table")
@router.post("/databases/drop")
@ -262,13 +398,39 @@ async def drop_database(
) -> Dict[str, Any]:
_ensure_admin_scope(currentUser)
db_name = payload.get("database")
configured_db = APP_CONFIG.get("DB_DATABASE") or APP_CONFIG.get("DB_NAME") or "poweron"
if not db_name or db_name != configured_db:
raise HTTPException(status_code=400, detail="Invalid database name")
# Get all configured database names
configured_dbs = []
app_db = APP_CONFIG.get("DB_APP_DATABASE")
if app_db:
configured_dbs.append(app_db)
chat_db = APP_CONFIG.get("DB_CHAT_DATABASE")
if chat_db:
configured_dbs.append(chat_db)
management_db = APP_CONFIG.get("DB_MANAGEMENT_DATABASE")
if management_db:
configured_dbs.append(management_db)
if not configured_dbs:
configured_dbs = ["poweron"]
if not db_name or db_name not in configured_dbs:
raise HTTPException(status_code=400, detail=f"Invalid database name. Available databases: {configured_dbs}")
try:
appInterface = getRootInterface()
conn = appInterface.db.connection
# Use the appropriate interface based on database name
if db_name == app_db:
interface = getRootInterface()
elif db_name == chat_db:
from modules.interfaces.interfaceChatObjects import getInterface as getChatInterface
interface = getChatInterface(currentUser)
elif db_name == management_db:
from modules.interfaces.interfaceComponentObjects import getInterface as getComponentInterface
interface = getComponentInterface(currentUser)
else:
raise HTTPException(status_code=400, detail="Database not found")
conn = interface.db.connection
with conn.cursor() as cursor:
# Drop all user tables (public schema) except system table
cursor.execute("""
@ -281,12 +443,12 @@ async def drop_database(
cursor.execute(f'DROP TABLE IF EXISTS "{tbl}" CASCADE')
dropped.append(tbl)
conn.commit()
logger.warning(f"Admin drop_database executed by {currentUser.id}: dropped tables: {dropped}")
logger.warning(f"Admin drop_database executed by {currentUser.id}: dropped tables from '{db_name}': {dropped}")
return {"droppedTables": dropped}
except Exception as e:
logger.error(f"Error dropping database tables: {str(e)}")
if appInterface and appInterface.db and appInterface.db.connection:
appInterface.db.connection.rollback()
if 'interface' in locals() and interface and interface.db and interface.db.connection:
interface.db.connection.rollback()
raise HTTPException(status_code=500, detail="Failed to drop database tables")

View file

@ -47,10 +47,10 @@ class ServiceCenter:
self._discoverMethods()
def _discoverMethods(self):
"""Dynamically discover all method classes and their actions in modules.methods package"""
"""Dynamically discover all method classes and their actions in modules methods package"""
try:
# Import the methods package
methodsPackage = importlib.import_module('modules.methods')
methodsPackage = importlib.import_module('modules.workflows.methods')
# Discover all modules in the package
for _, name, isPkg in pkgutil.iter_modules(methodsPackage.__path__):

View file

@ -567,7 +567,10 @@ class HandlingTasks:
state = TaskExecutionState(task_step)
# React mode path - check workflow mode instead of context
if isinstance(context, TaskContext) and hasattr(context, 'workflow') and context.workflow and getattr(context.workflow, 'workflowMode', 'Actionplan') == 'React':
workflow_mode = getattr(context.workflow, 'workflowMode', 'Actionplan') if context.workflow else 'Actionplan'
logger.info(f"Task execution - workflow mode: {workflow_mode}")
if isinstance(context, TaskContext) and hasattr(context, 'workflow') and context.workflow and workflow_mode == 'React':
logger.info(f"Using React mode execution with max_steps: {getattr(context.workflow, 'maxSteps', 5)}")
state.max_steps = max(1, int(getattr(context.workflow, 'maxSteps', 5)))
step = 1
last_review_dict = None
@ -579,6 +582,7 @@ class HandlingTasks:
try:
t0 = time.time()
selection = await self.plan_select(context)
logger.info(f"React step {step}: Selected action: {selection}")
result = await self.act_execute(context, selection, task_step, workflow, step)
observation = self.observe_build(result)
# Attach deterministic label for clarity
@ -630,6 +634,10 @@ class HandlingTasks:
feedback=feedback,
error=None if success else feedback
)
else:
# Actionplan mode execution
logger.info(f"Using Actionplan mode execution")
retry_context = context
max_retries = state.max_retries
for attempt in range(max_retries):

View file

@ -887,9 +887,17 @@ def createActionParameterPrompt(context: TaskContext, selected_action: Dict[str,
method = selected_action.get('method', '') if selected_action else ''
name = selected_action.get('name', '') if selected_action else ''
available_docs = _getAvailableDocuments(context.workflow) if context and context.workflow else "No documents available"
# Get action signature from service center
action_signature = ""
if service and hasattr(service, 'methods') and method in service.methods:
method_instance = service.methods[method]['instance']
action_signature = method_instance.getActionSignature(name)
return f"""Provide only the required parameters for this action.
SELECTED ACTION: {method}.{name}
ACTION SIGNATURE: {action_signature}
OBJECTIVE: {context.task_step.objective if context and context.task_step else ''}
AVAILABLE DOCUMENTS: {available_docs}
USER LANGUAGE: {user_language}
@ -899,6 +907,8 @@ RULES:
- Include user language if relevant.
- Reference documents only by exact labels available.
- Avoid unnecessary fields; host applies defaults.
- Use the ACTION SIGNATURE above to understand what parameters are required.
- Convert the objective into appropriate parameter values as needed.
RESPONSE FORMAT (JSON only):
{{"parameters":{{}}}}

View file

@ -21,10 +21,14 @@ class WorkflowManager:
self.chatInterface = chatInterface
self.currentUser = currentUser
self.handlingTasks = None
async def workflowStart(self, userInput: UserInputRequest, workflowId: Optional[str] = None) -> ChatWorkflow:
# Exported functions
async def workflowStart(self, userInput: UserInputRequest, workflowId: Optional[str] = None, workflowMode: str = "Actionplan") -> ChatWorkflow:
"""Starts a new workflow or continues an existing one, then launches processing."""
try:
# Debug log to check workflowMode parameter
logger.info(f"WorkflowManager received workflowMode: {workflowMode}")
currentTime = get_utc_timestamp()
if workflowId:
@ -80,6 +84,8 @@ class WorkflowManager:
"totalActions": 0,
"mandateId": self.chatInterface.mandateId,
"messageIds": [],
"workflowMode": workflowMode,
"maxSteps": 5 if workflowMode == "React" else 1, # Set maxSteps for React mode
"stats": {
"processingTime": None,
"tokenCount": None,
@ -91,6 +97,8 @@ class WorkflowManager:
}
workflow = self.chatInterface.createWorkflow(workflowData)
logger.info(f"Created workflow with mode: {getattr(workflow, 'workflowMode', 'NOT_SET')}")
logger.info(f"Workflow data passed: {workflowData.get('workflowMode', 'NOT_IN_DATA')}")
workflow.currentRound = 1
self.chatInterface.updateWorkflow(workflow.id, {"currentRound": 1})
self.chatInterface.updateWorkflowStats(workflow.id, bytesSent=0, bytesReceived=0)
@ -127,7 +135,9 @@ class WorkflowManager:
except Exception as e:
logger.error(f"Error stopping workflow: {str(e)}")
raise
# Main processor
async def _workflowProcess(self, userInput: UserInputRequest, workflow: ChatWorkflow) -> None:
"""Process a workflow with user input"""
try:
@ -143,7 +153,9 @@ class WorkflowManager:
except Exception as e:
self._handleWorkflowError(workflow, e)
# Helper functions
async def _sendFirstMessage(self, userInput: UserInputRequest, workflow: ChatWorkflow) -> ChatMessage:
"""Send first message to start workflow"""
try:
@ -205,7 +217,9 @@ class WorkflowManager:
task_plan = await handling.generateTaskPlan(userInput.prompt, workflow)
if not task_plan or not task_plan.tasks:
raise Exception("No tasks generated in task plan.")
logger.info(f"Executing workflow mode={getattr(workflow, 'workflowMode', 'Actionplan')} with {len(task_plan.tasks)} tasks")
workflow_mode = getattr(workflow, 'workflowMode', 'Actionplan')
logger.info(f"Workflow object attributes: {workflow.__dict__ if hasattr(workflow, '__dict__') else 'No __dict__'}")
logger.info(f"Executing workflow mode={workflow_mode} with {len(task_plan.tasks)} tasks")
return task_plan
async def _executeTasks(self, task_plan, workflow: ChatWorkflow) -> WorkflowResult: