From 1d4148e8b523f1024e87a9740b93a1a1353514bf Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Tue, 17 Mar 2026 20:26:11 +0100
Subject: [PATCH] codeeditor
---
.../workspace/routeFeatureWorkspace.py | 27 ++++++++++++++-----
modules/interfaces/interfaceDbKnowledge.py | 13 +++++++++
.../services/serviceAgent/agentLoop.py | 3 ---
.../services/serviceAgent/datamodelAgent.py | 5 +++-
.../services/serviceAgent/mainServiceAgent.py | 2 +-
.../services/serviceAgent/toolRegistry.py | 10 ++++---
6 files changed, 45 insertions(+), 15 deletions(-)
diff --git a/modules/features/workspace/routeFeatureWorkspace.py b/modules/features/workspace/routeFeatureWorkspace.py
index 7a5a92a7..62ebfdbc 100644
--- a/modules/features/workspace/routeFeatureWorkspace.py
+++ b/modules/features/workspace/routeFeatureWorkspace.py
@@ -9,7 +9,7 @@ and Playground into a single agent-driven workspace.
import logging
import json
import asyncio
-from typing import Dict, Optional, List
+from typing import Any, Dict, Optional, List
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Request, UploadFile, File
from fastapi.responses import StreamingResponse, JSONResponse
@@ -50,7 +50,8 @@ async def _getAiObjects() -> AiObjects:
return _aiObjects
-def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
+def _validateInstanceAccess(instanceId: str, context: RequestContext):
+ """Validate access and return (mandateId, instanceConfig) tuple."""
from modules.interfaces.interfaceDbApp import getRootInterface
rootInterface = getRootInterface()
instance = rootInterface.getFeatureInstance(instanceId)
@@ -59,7 +60,9 @@ def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
featureAccess = rootInterface.getFeatureAccess(str(context.user.id), instanceId)
if not featureAccess or not featureAccess.enabled:
raise HTTPException(status_code=403, detail="Access denied to this feature instance")
- return str(instance.mandateId) if instance.mandateId else None
+ mandateId = str(instance.mandateId) if instance.mandateId else None
+ instanceConfig = instance.config if hasattr(instance, "config") and instance.config else {}
+ return mandateId, instanceConfig
def _getChatInterface(context: RequestContext, featureInstanceId: str = None):
@@ -218,7 +221,7 @@ async def streamWorkspaceStart(
context: RequestContext = Depends(getRequestContext),
):
"""Start or continue a Workspace session with SSE streaming via serviceAgent."""
- mandateId = _validateInstanceAccess(instanceId, context)
+ mandateId, instanceConfig = _validateInstanceAccess(instanceId, context)
chatInterface = _getChatInterface(context, featureInstanceId=instanceId)
aiObjects = await _getAiObjects()
eventManager = get_event_manager()
@@ -260,6 +263,7 @@ async def streamWorkspaceStart(
chatInterface=chatInterface,
eventManager=eventManager,
userLanguage=userInput.userLanguage,
+ instanceConfig=instanceConfig,
)
)
eventManager.register_agent_task(queueId, agentTask)
@@ -312,6 +316,7 @@ async def _runWorkspaceAgent(
chatInterface,
eventManager,
userLanguage: str = "en",
+ instanceConfig: Dict[str, Any] = None,
):
"""Run the serviceAgent loop and forward events to the SSE queue."""
try:
@@ -356,12 +361,20 @@ async def _runWorkspaceAgent(
accumulatedText = ""
messagePersisted = False
+ _cfg = instanceConfig or {}
+ _toolSet = _cfg.get("toolSet", "core")
+ _agentCfg = _cfg.get("agentConfig")
+ from modules.serviceCenter.services.serviceAgent.datamodelAgent import AgentConfig
+ agentConfig = AgentConfig(**_agentCfg) if isinstance(_agentCfg, dict) else None
+
async for event in agentService.runAgent(
prompt=enrichedPrompt,
fileIds=fileIds,
workflowId=workflowId,
userLanguage=userLanguage,
conversationHistory=conversationHistory,
+ toolSet=_toolSet,
+ config=agentConfig,
):
if eventManager.is_cancelled(queueId):
logger.info(f"Agent cancelled by user for workflow {workflowId}")
@@ -1034,7 +1047,7 @@ async def getVoiceLanguages(
context: RequestContext = Depends(getRequestContext),
):
"""Return available TTS languages."""
- mandateId = _validateInstanceAccess(instanceId, context)
+ mandateId, _ = _validateInstanceAccess(instanceId, context)
from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
voiceInterface = getVoiceInterface(context.user, mandateId)
languagesResult = await voiceInterface.getAvailableLanguages()
@@ -1051,7 +1064,7 @@ async def getVoiceVoices(
context: RequestContext = Depends(getRequestContext),
):
"""Return available TTS voices for a given language."""
- mandateId = _validateInstanceAccess(instanceId, context)
+ mandateId, _ = _validateInstanceAccess(instanceId, context)
from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
voiceInterface = getVoiceInterface(context.user, mandateId)
voicesResult = await voiceInterface.getAvailableVoices(language)
@@ -1069,7 +1082,7 @@ async def testVoice(
):
"""Test a specific voice with a sample text."""
import base64
- mandateId = _validateInstanceAccess(instanceId, context)
+ mandateId, _ = _validateInstanceAccess(instanceId, context)
text = body.get("text", "Hallo, das ist ein Stimmtest.")
language = body.get("language", "de-DE")
voiceId = body.get("voiceId")
diff --git a/modules/interfaces/interfaceDbKnowledge.py b/modules/interfaces/interfaceDbKnowledge.py
index e15f19c2..fa83f1c8 100644
--- a/modules/interfaces/interfaceDbKnowledge.py
+++ b/modules/interfaces/interfaceDbKnowledge.py
@@ -195,6 +195,19 @@ class KnowledgeObjects:
if contentType:
recordFilter["contentType"] = contentType
+ if isShared and mandateId:
+ sharedIndexes = self.db.getRecordset(
+ FileContentIndex,
+ recordFilter={"mandateId": mandateId, "isShared": True},
+ )
+ sharedFileIds = [idx.get("id") if isinstance(idx, dict) else getattr(idx, "id", None) for idx in sharedIndexes]
+ sharedFileIds = [fid for fid in sharedFileIds if fid]
+ if not sharedFileIds:
+ return []
+ recordFilter.pop("userId", None)
+ recordFilter.pop("featureInstanceId", None)
+ recordFilter["fileId"] = sharedFileIds
+
return self.db.semanticSearch(
modelClass=ContentChunk,
vectorColumn="embedding",
diff --git a/modules/serviceCenter/services/serviceAgent/agentLoop.py b/modules/serviceCenter/services/serviceAgent/agentLoop.py
index 2c798680..2872c97a 100644
--- a/modules/serviceCenter/services/serviceAgent/agentLoop.py
+++ b/modules/serviceCenter/services/serviceAgent/agentLoop.py
@@ -24,9 +24,6 @@ from modules.shared.timeUtils import getUtcTimestamp
logger = logging.getLogger(__name__)
-MAX_RETRIES_PER_TOOL = 3
-RETRY_BASE_DELAY_S = 1.0
-
async def runAgentLoop(
prompt: str,
diff --git a/modules/serviceCenter/services/serviceAgent/datamodelAgent.py b/modules/serviceCenter/services/serviceAgent/datamodelAgent.py
index b786b550..93a8e966 100644
--- a/modules/serviceCenter/services/serviceAgent/datamodelAgent.py
+++ b/modules/serviceCenter/services/serviceAgent/datamodelAgent.py
@@ -48,6 +48,10 @@ class ToolDefinition(BaseModel):
default=None,
description="Feature scope for this tool (None = available to all)"
)
+ toolSet: Optional[str] = Field(
+ default=None,
+ description="Tool-set scope (None = available to all sets, e.g. 'core', 'codeeditor')"
+ )
class ToolCallRequest(BaseModel):
@@ -79,7 +83,6 @@ class AgentConfig(BaseModel):
"""Configuration for an agent run."""
maxRounds: int = Field(default=25, ge=1, le=100)
maxCostCHF: Optional[float] = Field(default=None, ge=0.0)
- entityCacheEnabled: bool = Field(default=False)
toolSet: str = Field(default="core")
temperature: Optional[float] = Field(default=None, ge=0.0, le=2.0)
diff --git a/modules/serviceCenter/services/serviceAgent/mainServiceAgent.py b/modules/serviceCenter/services/serviceAgent/mainServiceAgent.py
index 77a98bf1..79781804 100644
--- a/modules/serviceCenter/services/serviceAgent/mainServiceAgent.py
+++ b/modules/serviceCenter/services/serviceAgent/mainServiceAgent.py
@@ -1613,7 +1613,7 @@ def _registerCoreTools(registry: ToolRegistry, services):
result = await knowledgeService.extractContainerItem(fileId, containerPath)
if result:
return ToolResult(toolCallId="", toolName="extractContainerItem", success=True, data=str(result))
- return ToolResult(toolCallId="", toolName="extractContainerItem", success=True, data=f"On-demand extraction for '{containerPath}' queued.")
+ return ToolResult(toolCallId="", toolName="extractContainerItem", success=False, error=f"Item '{containerPath}' not found in container index for file {fileId}. On-demand extraction is not yet implemented.")
except Exception as e:
return ToolResult(toolCallId="", toolName="extractContainerItem", success=False, error=str(e))
diff --git a/modules/serviceCenter/services/serviceAgent/toolRegistry.py b/modules/serviceCenter/services/serviceAgent/toolRegistry.py
index 625001fb..d241bb93 100644
--- a/modules/serviceCenter/services/serviceAgent/toolRegistry.py
+++ b/modules/serviceCenter/services/serviceAgent/toolRegistry.py
@@ -22,7 +22,8 @@ class ToolRegistry:
def register(self, name: str, handler: Callable[..., Awaitable[ToolResult]],
description: str = "", parameters: Dict[str, Any] = None,
- readOnly: bool = False, featureType: str = None):
+ readOnly: bool = False, featureType: str = None,
+ toolSet: str = None):
"""Register a tool with its handler function."""
if name in self._tools:
logger.warning(f"Tool '{name}' already registered, overwriting")
@@ -32,10 +33,11 @@ class ToolRegistry:
description=description,
parameters=parameters or {},
readOnly=readOnly,
- featureType=featureType
+ featureType=featureType,
+ toolSet=toolSet,
)
self._handlers[name] = handler
- logger.debug(f"Registered tool: {name} (readOnly={readOnly})")
+ logger.debug(f"Registered tool: {name} (readOnly={readOnly}, toolSet={toolSet})")
def registerFromDefinition(self, definition: ToolDefinition,
handler: Callable[..., Awaitable[ToolResult]]):
@@ -54,6 +56,8 @@ class ToolRegistry:
tools = list(self._tools.values())
if featureType:
tools = [t for t in tools if t.featureType is None or t.featureType == featureType]
+ if toolSet:
+ tools = [t for t in tools if t.toolSet is None or t.toolSet == toolSet]
return tools
def getToolNames(self) -> List[str]: