# Copyright (c) 2026 Patrick Motsch # All rights reserved. """Workflow action: list Redmine tickets from the mirror with filters.""" import logging from typing import Any, Dict, List, Optional from modules.datamodels.datamodelChat import ActionResult from modules.features.redmine.serviceRedmine import listTickets from ._shared import resolveInstanceContext, ticketToDict logger = logging.getLogger(__name__) def _normalizeTrackerIds(value: Any) -> Optional[List[int]]: if value is None or value == "": return None if isinstance(value, int): return [value] if isinstance(value, str): value = [v.strip() for v in value.split(",") if v.strip()] if isinstance(value, list): ids: List[int] = [] for v in value: try: ids.append(int(v)) except (TypeError, ValueError): continue return ids or None return None async def listTicketsAction(self, parameters: Dict[str, Any]) -> ActionResult: """List Redmine tickets from the local mirror.""" try: user, mandateId, featureInstanceId = resolveInstanceContext(self.services, parameters) except ValueError as exc: return ActionResult.isFailure(error=str(exc)) trackerIds = _normalizeTrackerIds(parameters.get("trackerIds")) statusFilter = (parameters.get("status") or "*").lower() if statusFilter not in {"*", "open", "closed"}: statusFilter = "*" updatedFrom = parameters.get("dateFrom") or None updatedTo = parameters.get("dateTo") or None assignedToId: Optional[int] = None if parameters.get("assignedToId") not in (None, ""): try: assignedToId = int(parameters["assignedToId"]) except (TypeError, ValueError): return ActionResult.isFailure(error="assignedToId must be an int") try: tickets = listTickets( user, mandateId, featureInstanceId, trackerIds=trackerIds, statusFilter=statusFilter, updatedOnFrom=updatedFrom, updatedOnTo=updatedTo, assignedToId=assignedToId, ) except Exception as exc: logger.exception("redmine.listTickets failed") return ActionResult.isFailure(error=f"List tickets failed: {exc}") # AI-friendly pagination: always capped so we don't accidentally feed a # 20k-ticket dump into a context window. Callers that need more must # paginate via filters. limit = 100 try: limit = max(1, min(500, int(parameters.get("limit") or 100))) except (TypeError, ValueError): limit = 100 truncated = tickets[:limit] return ActionResult.isSuccess(data={ "count": len(truncated), "totalMatched": len(tickets), "truncated": len(tickets) > limit, "tickets": [ticketToDict(t) for t in truncated], })