diff --git a/modules/aicore/aicorePluginAnthropic.py b/modules/aicore/aicorePluginAnthropic.py index 1119f115..3b5eccda 100644 --- a/modules/aicore/aicorePluginAnthropic.py +++ b/modules/aicore/aicorePluginAnthropic.py @@ -13,6 +13,35 @@ from modules.datamodels.datamodelAi import AiModel, PriorityEnum, ProcessingMode # Configure logger logger = logging.getLogger(__name__) + +def _supportsCustomTemperature(modelName: str) -> bool: + """Check whether an Anthropic model accepts a custom ``temperature``. + + Anthropic's Extended-Thinking models (Claude 4.7 Opus and the + upcoming 4.7 Sonnet/Haiku, plus all 5.x and beyond) reject every + ``temperature`` value with HTTP 400 + ``{"error": "`temperature` is deprecated for this model."}`` -- + only the model's internal default is accepted. Older Claude 4.5 / + 4.6 models still accept any value in [0, 1]. + + Returns: + True if ``temperature`` may be sent; False if it must be omitted. + """ + if not modelName: + return True + name = modelName.lower() + if name.startswith("claude-opus-4-7"): + return False + if name.startswith("claude-sonnet-4-7"): + return False + if name.startswith("claude-haiku-4-7"): + return False + # 5.x and beyond: same Extended-Thinking family, no custom temperature. + if name.startswith("claude-opus-5") or name.startswith("claude-sonnet-5") or name.startswith("claude-haiku-5"): + return False + return True + + def loadConfigData(): """Load configuration data for Anthropic connector""" return { @@ -276,9 +305,12 @@ class AiAnthropic(BaseConnectorAi): payload: Dict[str, Any] = { "model": model.name, "messages": converted_messages, - "temperature": temperature, } - + # Extended-Thinking models (claude-opus-4-7 etc.) reject any + # `temperature` value -- only the model default is accepted. + if _supportsCustomTemperature(model.name): + payload["temperature"] = temperature + # Anthropic requires max_tokens - use provided value or throw error if maxTokens is None: raise ValueError("maxTokens must be provided for Anthropic API calls") @@ -381,10 +413,11 @@ class AiAnthropic(BaseConnectorAi): payload: Dict[str, Any] = { "model": model.name, "messages": converted, - "temperature": temperature, "max_tokens": model.maxTokens, "stream": True, } + if _supportsCustomTemperature(model.name): + payload["temperature"] = temperature if system_prompt: payload["system"] = system_prompt if modelCall.tools: @@ -608,10 +641,10 @@ class AiAnthropic(BaseConnectorAi): if systemPrompt: payload["system"] = systemPrompt - - # Set temperature from model - payload["temperature"] = temperature - + + if _supportsCustomTemperature(model.name): + payload["temperature"] = temperature + # Make API call with headers from httpClient (which includes anthropic-version) response = await self.httpClient.post( "https://api.anthropic.com/v1/messages", diff --git a/modules/connectors/providerInfomaniak/connectorInfomaniak.py b/modules/connectors/providerInfomaniak/connectorInfomaniak.py index 9153ad73..cb592261 100644 --- a/modules/connectors/providerInfomaniak/connectorInfomaniak.py +++ b/modules/connectors/providerInfomaniak/connectorInfomaniak.py @@ -101,13 +101,22 @@ async def _infomaniakDownload( endpoint: str, baseUrl: str = _API_BASE, ) -> Optional[bytes]: - """Binary download from an Infomaniak host. Returns bytes or ``None``.""" + """Binary download from an Infomaniak host. Returns bytes or ``None``. + + Unlike :func:`_infomaniakGet`, this follows redirects: kDrive's + ``/2/drive/{driveId}/files/{fileId}/download`` answers with + ``302 -> presigned CDN URL`` (standard for bandwidth-heavy + transfers), and the same pattern shows up on Calendar/Contacts + export endpoints. Refusing to follow would lose every download. + The Authorization header is preserved across the redirect by + aiohttp because the host is the same Infomaniak property. + """ url = f"{baseUrl.rstrip('/')}/{endpoint.lstrip('/')}" headers = {"Authorization": f"Bearer {token}"} timeout = aiohttp.ClientTimeout(total=120) try: async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.get(url, headers=headers, allow_redirects=False) as resp: + async with session.get(url, headers=headers, allow_redirects=True) as resp: if resp.status == 200: return await resp.read() logger.warning( diff --git a/modules/datamodels/datamodelDataSource.py b/modules/datamodels/datamodelDataSource.py index 10d2976c..d9e40bde 100644 --- a/modules/datamodels/datamodelDataSource.py +++ b/modules/datamodels/datamodelDataSource.py @@ -26,7 +26,12 @@ class DataSource(PowerOnModel): json_schema_extra={"label": "Verbindungs-ID", "fk_target": {"db": "poweron_app", "table": "UserConnection", "labelField": "externalUsername"}}, ) sourceType: str = Field( - description="sharepointFolder, googleDriveFolder, outlookFolder, ftpFolder, clickupList (path under /team/...)", + description=( + "sharepointFolder, onedriveFolder, googleDriveFolder, " + "outlookFolder, gmailFolder, ftpFolder, clickupList " + "(path under /team/...), kdriveFolder, calendarFolder, " + "contactFolder" + ), json_schema_extra={"label": "Quellentyp"}, ) path: str = Field( diff --git a/modules/features/workspace/routeFeatureWorkspace.py b/modules/features/workspace/routeFeatureWorkspace.py index 3fa85c6c..3e1a54b7 100644 --- a/modules/features/workspace/routeFeatureWorkspace.py +++ b/modules/features/workspace/routeFeatureWorkspace.py @@ -188,6 +188,9 @@ _SOURCE_TYPE_TO_SERVICE = { "gmailFolder": "gmail", "ftpFolder": "files", "clickupList": "clickup", + "kdriveFolder": "kdrive", + "calendarFolder": "calendar", + "contactFolder": "contact", } diff --git a/modules/serviceCenter/services/serviceAgent/coreTools/_dataSourceTools.py b/modules/serviceCenter/services/serviceAgent/coreTools/_dataSourceTools.py index a369530b..abe24c95 100644 --- a/modules/serviceCenter/services/serviceAgent/coreTools/_dataSourceTools.py +++ b/modules/serviceCenter/services/serviceAgent/coreTools/_dataSourceTools.py @@ -37,6 +37,11 @@ def _registerDataSourceTools(registry: ToolRegistry, services): return getattr(chatService, "interfaceDbComponent", None) # ---- DataSource convenience tools ---- + # Maps the FE-side `sourceType` literal (see SourcesTab.tsx + # `_SERVICE_TO_SOURCE_TYPE`) to the Connector's `service` key in + # `_SERVICE_MAP`. Keep this table in sync with both the FE and the + # Connector `_SERVICE_MAP` entries -- a missing row produces + # "Service '' not available" in the agent tools. _SOURCE_TYPE_TO_SERVICE = { "sharepointFolder": "sharepoint", "onedriveFolder": "onedrive", @@ -45,6 +50,9 @@ def _registerDataSourceTools(registry: ToolRegistry, services): "gmailFolder": "gmail", "ftpFolder": "files", "clickupList": "clickup", + "kdriveFolder": "kdrive", + "calendarFolder": "calendar", + "contactFolder": "contact", } async def _resolveDataSource(dsId: str):