cleaned model space centralized

This commit is contained in:
ValueOn AG 2025-09-25 16:59:44 +02:00
parent 842fe46a87
commit 07a2d279df
107 changed files with 1651 additions and 7101 deletions

View file

@ -1,15 +1,12 @@
"""ClickUp connector for CRUD operations (compatible with TicketInterface).""" """ClickUp connector for CRUD operations (compatible with TicketInterface).
This module defines its own minimal abstractions to avoid coupling.
"""
from dataclasses import dataclass
from typing import Optional from typing import Optional
import logging import logging
import aiohttp import aiohttp
from modules.datamodels.datamodelTickets import TicketBase, TicketFieldAttribute
from modules.interfaces.interfaceTicketModel import (
TicketBase,
TicketFieldAttribute,
Task,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -68,11 +65,11 @@ class ConnectorTicketClickup(TicketBase):
logger.error(f"ClickUp read_attributes error: {e}") logger.error(f"ClickUp read_attributes error: {e}")
return attributes return attributes
async def read_tasks(self, *, limit: int = 0) -> list[Task]: async def read_tasks(self, *, limit: int = 0) -> list[dict]:
"""Read tasks from ClickUp, always returning full task records. """Read tasks from ClickUp, always returning full task records.
If list_id is set, read from that list; otherwise read from team. If list_id is set, read from that list; otherwise read from team.
""" """
tasks: list[Task] = [] tasks: list[dict] = []
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
page = 0 page = 0
@ -94,7 +91,7 @@ class ConnectorTicketClickup(TicketBase):
data = await response.json() data = await response.json()
items = data.get("tasks", []) items = data.get("tasks", [])
for item in items: for item in items:
tasks.append(Task(data=item)) tasks.append(item)
if limit and len(tasks) >= limit: if limit and len(tasks) >= limit:
return tasks return tasks
@ -105,12 +102,11 @@ class ConnectorTicketClickup(TicketBase):
logger.error(f"ClickUp read_tasks error: {e}") logger.error(f"ClickUp read_tasks error: {e}")
return tasks return tasks
async def write_tasks(self, tasklist: list[Task]) -> None: async def write_tasks(self, tasklist: list[dict]) -> None:
"""Update tasks in ClickUp. Expects Task.data to contain {'ID' or 'id' or 'task_id', 'fields': {...}}""" """Update tasks in ClickUp. Expects each item to contain {'ID' or 'id' or 'task_id', 'fields': {...}}"""
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
for task in tasklist: for data in tasklist:
data = task.data
taskId = data.get("ID") or data.get("id") or data.get("task_id") taskId = data.get("ID") or data.get("id") or data.get("task_id")
fields = data.get("fields", {}) fields = data.get("fields", {})
if not taskId or not isinstance(fields, dict) or not fields: if not taskId or not isinstance(fields, dict) or not fields:

View file

@ -1,12 +1,13 @@
"""Jira connector for CRUD operations (neutralized to generic ticket interface).""" """Jira connector for CRUD operations (neutralized to generic ticket interface).
This module defines its own minimal abstractions to avoid coupling.
"""
from dataclasses import dataclass
import logging import logging
import aiohttp import aiohttp
import asyncio import asyncio
import json import json
from modules.datamodels.datamodelTickets import TicketBase, TicketFieldAttribute
from modules.interfaces.interfaceTicketModel import (TicketBase, TicketFieldAttribute, Task, )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -129,7 +130,7 @@ class ConnectorTicketJira(TicketBase):
logger.error(f"Error while calling fields API: {str(e)}") logger.error(f"Error while calling fields API: {str(e)}")
return [] return []
async def read_tasks(self, *, limit: int = 0) -> list[Task]: async def read_tasks(self, *, limit: int = 0) -> list[dict]:
""" """
Read tasks from Jira with pagination support. Read tasks from Jira with pagination support.
@ -137,7 +138,7 @@ class ConnectorTicketJira(TicketBase):
limit: Maximum number of tasks to retrieve. 0 means no limit. limit: Maximum number of tasks to retrieve. 0 means no limit.
Returns: Returns:
list[Task]: List of tasks with their data list[dict]: List of tasks with their data
""" """
# Build JQL dynamically; allow empty or '*' issue_type to mean "all types" # Build JQL dynamically; allow empty or '*' issue_type to mean "all types"
if self.ticketType and self.ticketType != "*": if self.ticketType and self.ticketType != "*":
@ -148,7 +149,7 @@ class ConnectorTicketJira(TicketBase):
# Initialize variables for pagination (cursor-based /search/jql) # Initialize variables for pagination (cursor-based /search/jql)
max_results = 100 max_results = 100
next_page_token: str | None = None next_page_token: str | None = None
tasks = [] tasks: list[dict] = []
page_counter = 0 page_counter = 0
max_pages_safety_cap = 1000 max_pages_safety_cap = 1000
seen_issue_ids: set[str] = set() seen_issue_ids: set[str] = set()
@ -202,8 +203,7 @@ class ConnectorTicketJira(TicketBase):
continue continue
if issue_id: if issue_id:
seen_issue_ids.add(issue_id) seen_issue_ids.add(issue_id)
task = Task(data=issue) tasks.append(issue)
tasks.append(task)
new_items_added += 1 new_items_added += 1
# Check limit # Check limit
@ -253,20 +253,19 @@ class ConnectorTicketJira(TicketBase):
logger.error(f"Unexpected error while fetching Jira tasks: {str(e)}") logger.error(f"Unexpected error while fetching Jira tasks: {str(e)}")
raise raise
async def write_tasks(self, tasklist: list[Task]) -> None: async def write_tasks(self, tasklist: list[dict]) -> None:
""" """
Write/update tasks to Jira. Write/update tasks to Jira.
Args: Args:
tasklist: List of Task objects containing task data to update tasklist: List of dicts containing task data to update
""" """
headers = {"Accept": "application/json", "Content-Type": "application/json"} headers = {"Accept": "application/json", "Content-Type": "application/json"}
auth = aiohttp.BasicAuth(self.apiUsername, self.apiToken) auth = aiohttp.BasicAuth(self.apiUsername, self.apiToken)
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
for task in tasklist: for task_data in tasklist:
task_data = task.data
task_id = ( task_id = (
task_data.get("ID") task_data.get("ID")
or task_data.get("id") or task_data.get("id")
@ -274,7 +273,7 @@ class ConnectorTicketJira(TicketBase):
) )
if not task_id: if not task_id:
logger.warning("Task missing ID or key, skipping update") logger.warning("Ticket update missing ID or key, skipping")
continue continue
# Extract fields to update from task data # Extract fields to update from task data

View file

@ -1,62 +1,50 @@
"""Tavily web search class.""" """Tavily web search class.
"""
import logging import logging
import os import asyncio
from dataclasses import dataclass from dataclasses import dataclass
from modules.interfaces.interfaceWebModel import ( from tavily import AsyncTavilyClient
WebCrawlBase, from modules.shared.configuration import APP_CONFIG
WebCrawlDocumentData, from modules.shared.timezoneUtils import get_utc_timestamp
WebCrawlRequest, from modules.datamodels.datamodelWeb import (
WebCrawlResultItem,
WebScrapeActionDocument,
WebScrapeActionResult,
WebScrapeBase,
WebScrapeDocumentData,
WebScrapeRequest,
WebScrapeResultItem,
WebSearchBase,
WebSearchRequest,
WebSearchActionResult, WebSearchActionResult,
WebSearchActionDocument, WebSearchActionDocument,
WebSearchDocumentData, WebSearchDocumentData,
WebSearchResultItem, WebSearchResultItem,
WebCrawlActionDocument,
WebCrawlActionResult, WebCrawlActionResult,
get_web_search_min_results, WebCrawlActionDocument,
get_web_search_max_results, WebCrawlDocumentData,
WebCrawlResultItem,
WebScrapeActionResult,
WebScrapeActionDocument,
WebSearchDocumentData as WebScrapeDocumentData,
WebScrapeResultItem,
) )
# from modules.interfaces.interfaceChatModel import ActionResult, ActionDocument
from tavily import AsyncTavilyClient
from modules.shared.timezoneUtils import get_utc_timestamp
from modules.shared.configuration import APP_CONFIG
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Cached configuration values are loaded into the connector instance on creation
@dataclass @dataclass
class TavilySearchResult: class WebSearchResult:
title: str title: str
url: str url: str
@dataclass @dataclass
class TavilyCrawlResult: class WebCrawlResult:
url: str url: str
content: str content: str
@dataclass @dataclass
class ConnectorTavily(WebSearchBase, WebCrawlBase, WebScrapeBase): class ConnectorWeb:
client: AsyncTavilyClient = None client: AsyncTavilyClient = None
# Cached settings loaded at initialization time # Cached settings loaded at initialization time
crawl_timeout: int = 30 crawl_timeout: int = 30
crawl_max_retries: int = 3 crawl_max_retries: int = 3
crawl_retry_delay: int = 2 crawl_retry_delay: int = 2
# Cached web search constraints (camelCase per project style)
webSearchMinResults: int = 1
webSearchMaxResults: int = 20
@classmethod @classmethod
async def create(cls): async def create(cls):
@ -72,16 +60,14 @@ class ConnectorTavily(WebSearchBase, WebCrawlBase, WebScrapeBase):
crawl_timeout=crawl_timeout, crawl_timeout=crawl_timeout,
crawl_max_retries=crawl_max_retries, crawl_max_retries=crawl_max_retries,
crawl_retry_delay=crawl_retry_delay, crawl_retry_delay=crawl_retry_delay,
webSearchMinResults=int(APP_CONFIG.get("Web_Search_MIN_RESULTS", "1")),
webSearchMaxResults=int(APP_CONFIG.get("Web_Search_MAX_RESULTS", "20")),
) )
async def search_urls(self, request: WebSearchRequest) -> WebSearchActionResult: # Standardized methods returning ActionResults for the interface to consume
"""Handles the web search request. async def search(self, request) -> "WebSearchActionResult":
Takes a query and returns a list of URLs.
"""
# Step 1: Search
try: try:
search_results = await self._search( raw_results = await self._search(
query=request.query, query=request.query,
max_results=request.max_results, max_results=request.max_results,
search_depth=request.search_depth, search_depth=request.search_depth,
@ -96,33 +82,59 @@ class ConnectorTavily(WebSearchBase, WebCrawlBase, WebScrapeBase):
except Exception as e: except Exception as e:
return WebSearchActionResult(success=False, error=str(e)) return WebSearchActionResult(success=False, error=str(e))
# Step 2: Build ActionResult result_items = [
try: WebSearchResultItem(title=result.title, url=result.url)
result = self._build_search_action_result(search_results, request.query) for result in raw_results
except Exception as e: ]
return WebSearchActionResult(success=False, error=str(e))
return result document_data = WebSearchDocumentData(
query=request.query,
results=result_items,
total_count=len(result_items),
)
async def crawl_urls(self, request: WebCrawlRequest) -> WebCrawlActionResult: document = WebSearchActionDocument(
"""Crawls the given URLs and returns the extracted text content.""" documentName=f"web_search_results_{get_utc_timestamp()}.json",
# Step 1: Crawl documentData=document_data,
mimeType="application/json",
)
return WebSearchActionResult(
success=True, documents=[document], resultLabel="web_search_results"
)
async def crawl(self, request) -> "WebCrawlActionResult":
try: try:
crawl_results = await self._crawl(request.urls) raw_results = await self._crawl(
[str(u) for u in request.urls],
extract_depth=request.extract_depth,
format=request.format,
)
except Exception as e: except Exception as e:
return WebCrawlActionResult(success=False, error=str(e)) return WebCrawlActionResult(success=False, error=str(e))
# Step 2: Build ActionResult result_items = [
try: WebCrawlResultItem(url=result.url, content=result.content)
result = self._build_crawl_action_result(crawl_results, request.urls) for result in raw_results
except Exception as e: ]
return WebCrawlActionResult(success=False, error=str(e))
return result document_data = WebCrawlDocumentData(
urls=[str(u) for u in request.urls],
results=result_items,
total_count=len(result_items),
)
async def scrape(self, request: WebScrapeRequest) -> WebScrapeActionResult: document = WebCrawlActionDocument(
"""Turns a query in a list of urls with extracted content.""" documentName=f"web_crawl_results_{get_utc_timestamp()}.json",
# Step 1: Search documentData=document_data,
mimeType="application/json",
)
return WebCrawlActionResult(
success=True, documents=[document], resultLabel="web_crawl_results"
)
async def scrape(self, request) -> "WebScrapeActionResult":
try: try:
search_results = await self._search( search_results = await self._search(
query=request.query, query=request.query,
@ -139,7 +151,6 @@ class ConnectorTavily(WebSearchBase, WebCrawlBase, WebScrapeBase):
except Exception as e: except Exception as e:
return WebScrapeActionResult(success=False, error=str(e)) return WebScrapeActionResult(success=False, error=str(e))
# Step 2: Crawl
try: try:
urls = [result.url for result in search_results] urls = [result.url for result in search_results]
crawl_results = await self._crawl( crawl_results = await self._crawl(
@ -150,13 +161,90 @@ class ConnectorTavily(WebSearchBase, WebCrawlBase, WebScrapeBase):
except Exception as e: except Exception as e:
return WebScrapeActionResult(success=False, error=str(e)) return WebScrapeActionResult(success=False, error=str(e))
# Step 3: Build ActionResult result_items = [
try: WebScrapeResultItem(url=result.url, content=result.content)
result = self._build_scrape_action_result(crawl_results, request.query) for result in crawl_results
except Exception as e: ]
return WebScrapeActionResult(success=False, error=str(e))
return result document_data = WebScrapeDocumentData(
query=request.query,
results=result_items,
total_count=len(result_items),
)
document = WebScrapeActionDocument(
documentName=f"web_scrape_results_{get_utc_timestamp()}.json",
documentData=document_data,
mimeType="application/json",
)
return WebScrapeActionResult(
success=True, documents=[document], resultLabel="web_scrape_results"
)
async def _search_urls_raw(self,
*,
query: str,
max_results: int,
search_depth: str | None = None,
time_range: str | None = None,
topic: str | None = None,
include_domains: list[str] | None = None,
exclude_domains: list[str] | None = None,
language: str | None = None,
include_answer: bool | None = None,
include_raw_content: bool | None = None,
) -> list["WebSearchResult"]:
return await self._search(
query=query,
max_results=max_results,
search_depth=search_depth,
time_range=time_range,
topic=topic,
include_domains=include_domains,
exclude_domains=exclude_domains,
language=language,
include_answer=include_answer,
include_raw_content=include_raw_content,
)
async def _crawl_urls_raw(self,
*,
urls: list[str],
extract_depth: str | None = None,
format: str | None = None,
) -> list["WebCrawlResult"]:
return await self._crawl(urls, extract_depth=extract_depth, format=format)
async def _scrape_raw(self,
*,
query: str,
max_results: int,
search_depth: str | None = None,
time_range: str | None = None,
topic: str | None = None,
include_domains: list[str] | None = None,
exclude_domains: list[str] | None = None,
language: str | None = None,
include_answer: bool | None = None,
include_raw_content: bool | None = None,
extract_depth: str | None = None,
format: str | None = None,
) -> list["WebCrawlResult"]:
search_results = await self._search(
query=query,
max_results=max_results,
search_depth=search_depth,
time_range=time_range,
topic=topic,
include_domains=include_domains,
exclude_domains=exclude_domains,
language=language,
include_answer=include_answer,
include_raw_content=include_raw_content,
)
urls = [result.url for result in search_results]
return await self._crawl(urls, extract_depth=extract_depth, format=format)
async def _search( async def _search(
self, self,
@ -170,11 +258,11 @@ class ConnectorTavily(WebSearchBase, WebCrawlBase, WebScrapeBase):
language: str | None = None, language: str | None = None,
include_answer: bool | None = None, include_answer: bool | None = None,
include_raw_content: bool | None = None, include_raw_content: bool | None = None,
) -> list[TavilySearchResult]: ) -> list[WebSearchResult]:
"""Calls the Tavily API to perform a web search.""" """Calls the Tavily API to perform a web search."""
# Make sure max_results is within the allowed range # Make sure max_results is within the allowed range (use cached values)
min_results = get_web_search_min_results() min_results = self.webSearchMinResults
max_allowed_results = get_web_search_max_results() max_allowed_results = self.webSearchMaxResults
if max_results < min_results or max_results > max_allowed_results: if max_results < min_results or max_results > max_allowed_results:
raise ValueError(f"max_results must be between {min_results} and {max_allowed_results}") raise ValueError(f"max_results must be between {min_results} and {max_allowed_results}")
@ -201,45 +289,17 @@ class ConnectorTavily(WebSearchBase, WebCrawlBase, WebScrapeBase):
response = await self.client.search(**kwargs) response = await self.client.search(**kwargs)
return [ return [
TavilySearchResult(title=result["title"], url=result["url"]) WebSearchResult(title=result["title"], url=result["url"])
for result in response["results"] for result in response["results"]
] ]
def _build_search_action_result(
self, search_results: list[TavilySearchResult], query: str = ""
) -> WebSearchActionResult:
"""Builds the ActionResult from the search results."""
# Convert to result items
result_items = [
WebSearchResultItem(title=result.title, url=result.url)
for result in search_results
]
# Create document data with all results
document_data = WebSearchDocumentData(
query=query, results=result_items, total_count=len(result_items)
)
# Create single document
document = WebSearchActionDocument(
documentName=f"web_search_results_{get_utc_timestamp()}.json",
documentData=document_data,
mimeType="application/json",
)
return WebSearchActionResult(
success=True, documents=[document], resultLabel="web_search_results"
)
async def _crawl( async def _crawl(
self, self,
urls: list, urls: list,
extract_depth: str | None = None, extract_depth: str | None = None,
format: str | None = None, format: str | None = None,
) -> list[TavilyCrawlResult]: ) -> list[WebCrawlResult]:
"""Calls the Tavily API to extract text content from URLs with retry logic.""" """Calls the Tavily API to extract text content from URLs with retry logic."""
import asyncio
max_retries = self.crawl_max_retries max_retries = self.crawl_max_retries
retry_delay = self.crawl_retry_delay retry_delay = self.crawl_retry_delay
timeout = self.crawl_timeout timeout = self.crawl_timeout
@ -258,7 +318,7 @@ class ConnectorTavily(WebSearchBase, WebCrawlBase, WebScrapeBase):
) )
return [ return [
TavilyCrawlResult(url=result["url"], content=result["raw_content"]) WebCrawlResult(url=result["url"], content=result["raw_content"])
for result in response["results"] for result in response["results"]
] ]
@ -277,59 +337,3 @@ class ConnectorTavily(WebSearchBase, WebCrawlBase, WebScrapeBase):
await asyncio.sleep(retry_delay) await asyncio.sleep(retry_delay)
else: else:
raise Exception(f"Crawl failed after {max_retries + 1} attempts: {str(e)}") raise Exception(f"Crawl failed after {max_retries + 1} attempts: {str(e)}")
def _build_crawl_action_result(
self, crawl_results: list[TavilyCrawlResult], urls: list[str] = None
) -> WebCrawlActionResult:
"""Builds the ActionResult from the crawl results."""
# Convert to result items
result_items = [
WebCrawlResultItem(url=result.url, content=result.content)
for result in crawl_results
]
# Create document data with all results
document_data = WebCrawlDocumentData(
urls=urls or [result.url for result in crawl_results],
results=result_items,
total_count=len(result_items),
)
# Create single document
document = WebCrawlActionDocument(
documentName=f"web_crawl_results_{get_utc_timestamp()}.json",
documentData=document_data,
mimeType="application/json",
)
return WebCrawlActionResult(
success=True, documents=[document], resultLabel="web_crawl_results"
)
def _build_scrape_action_result(
self, crawl_results: list[TavilyCrawlResult], query: str = ""
) -> WebScrapeActionResult:
"""Builds the ActionResult from the scrape results."""
# Convert to result items
result_items = [
WebScrapeResultItem(url=result.url, content=result.content)
for result in crawl_results
]
# Create document data with all results
document_data = WebScrapeDocumentData(
query=query,
results=result_items,
total_count=len(result_items),
)
# Create single document
document = WebScrapeActionDocument(
documentName=f"web_scrape_results_{get_utc_timestamp()}.json",
documentData=document_data,
mimeType="application/json",
)
return WebScrapeActionResult(
success=True, documents=[document], resultLabel="web_scrape_results"
)

View file

@ -0,0 +1,17 @@
"""
Unified modules.datamodels package.
Usage examples:
from modules.datamodels import ai
from modules.datamodels import web
"""
from . import datamodelAi as ai
from . import datamodelWeb as web
from . import datamodelUam as uam
from . import datamodelSecurity as security
from . import datamodelNeutralizer as neutralizer
from . import datamodelWorkflow as workflow
from . import datamodelChat as chat
from . import datamodelFiles as files
from . import datamodelVoice as voice
from . import datamodelUtils as utils

View file

@ -28,3 +28,5 @@ class AiCallResponse(BaseModel):
modelName: str = Field(description="Selected model name") modelName: str = Field(description="Selected model name")
usedTokens: Optional[int] = Field(default=None, description="Estimated used tokens") usedTokens: Optional[int] = Field(default=None, description="Estimated used tokens")
costEstimate: Optional[float] = Field(default=None, description="Estimated cost of the call") costEstimate: Optional[float] = Field(default=None, description="Estimated cost of the call")

View file

@ -0,0 +1,262 @@
"""Chat models: ChatWorkflow, ChatMessage, ChatLog, ChatStat, ChatDocument."""
from typing import List, Dict, Any, Optional
from pydantic import BaseModel, Field
from modules.shared.attributeUtils import register_model_labels, ModelMixin
from modules.shared.timezoneUtils import get_utc_timestamp
import uuid
class ChatStat(BaseModel, ModelMixin):
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
workflowId: Optional[str] = Field(None, description="Foreign key to workflow (for workflow stats)")
messageId: Optional[str] = Field(None, description="Foreign key to message (for message stats)")
processingTime: Optional[float] = Field(None, description="Processing time in seconds")
tokenCount: Optional[int] = Field(None, description="Number of tokens processed")
bytesSent: Optional[int] = Field(None, description="Number of bytes sent")
bytesReceived: Optional[int] = Field(None, description="Number of bytes received")
successRate: Optional[float] = Field(None, description="Success rate of operations")
errorCount: Optional[int] = Field(None, description="Number of errors encountered")
register_model_labels(
"ChatStat",
{"en": "Chat Statistics", "fr": "Statistiques de chat"},
{
"id": {"en": "ID", "fr": "ID"},
"workflowId": {"en": "Workflow ID", "fr": "ID du workflow"},
"messageId": {"en": "Message ID", "fr": "ID du message"},
"processingTime": {"en": "Processing Time", "fr": "Temps de traitement"},
"tokenCount": {"en": "Token Count", "fr": "Nombre de tokens"},
"bytesSent": {"en": "Bytes Sent", "fr": "Octets envoyés"},
"bytesReceived": {"en": "Bytes Received", "fr": "Octets reçus"},
"successRate": {"en": "Success Rate", "fr": "Taux de succès"},
"errorCount": {"en": "Error Count", "fr": "Nombre d'erreurs"},
},
)
class ChatLog(BaseModel, ModelMixin):
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
workflowId: str = Field(description="Foreign key to workflow")
message: str = Field(description="Log message")
type: str = Field(description="Log type (info, warning, error, etc.)")
timestamp: float = Field(default_factory=get_utc_timestamp, description="When the log entry was created (UTC timestamp in seconds)")
status: Optional[str] = Field(None, description="Status of the log entry")
progress: Optional[float] = Field(None, description="Progress indicator (0.0 to 1.0)")
performance: Optional[Dict[str, Any]] = Field(None, description="Performance metrics")
register_model_labels(
"ChatLog",
{"en": "Chat Log", "fr": "Journal de chat"},
{
"id": {"en": "ID", "fr": "ID"},
"workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
"message": {"en": "Message", "fr": "Message"},
"type": {"en": "Type", "fr": "Type"},
"timestamp": {"en": "Timestamp", "fr": "Horodatage"},
"status": {"en": "Status", "fr": "Statut"},
"progress": {"en": "Progress", "fr": "Progression"},
"performance": {"en": "Performance", "fr": "Performance"},
},
)
class ChatDocument(BaseModel, ModelMixin):
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
messageId: str = Field(description="Foreign key to message")
fileId: str = Field(description="Foreign key to file")
fileName: str = Field(description="Name of the file")
fileSize: int = Field(description="Size of the file")
mimeType: str = Field(description="MIME type of the file")
roundNumber: Optional[int] = Field(None, description="Round number in workflow")
taskNumber: Optional[int] = Field(None, description="Task number within round")
actionNumber: Optional[int] = Field(None, description="Action number within task")
actionId: Optional[str] = Field(None, description="ID of the action that created this document")
register_model_labels(
"ChatDocument",
{"en": "Chat Document", "fr": "Document de chat"},
{
"id": {"en": "ID", "fr": "ID"},
"messageId": {"en": "Message ID", "fr": "ID du message"},
"fileId": {"en": "File ID", "fr": "ID du fichier"},
"fileName": {"en": "File Name", "fr": "Nom du fichier"},
"fileSize": {"en": "File Size", "fr": "Taille du fichier"},
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
"roundNumber": {"en": "Round Number", "fr": "Numéro de tour"},
"taskNumber": {"en": "Task Number", "fr": "Numéro de tâche"},
"actionNumber": {"en": "Action Number", "fr": "Numéro d'action"},
"actionId": {"en": "Action ID", "fr": "ID de l'action"},
},
)
class ContentMetadata(BaseModel, ModelMixin):
size: int = Field(description="Content size in bytes")
pages: Optional[int] = Field(None, description="Number of pages for multi-page content")
error: Optional[str] = Field(None, description="Processing error if any")
width: Optional[int] = Field(None, description="Width in pixels for images/videos")
height: Optional[int] = Field(None, description="Height in pixels for images/videos")
colorMode: Optional[str] = Field(None, description="Color mode")
fps: Optional[float] = Field(None, description="Frames per second for videos")
durationSec: Optional[float] = Field(None, description="Duration in seconds for media")
mimeType: str = Field(description="MIME type of the content")
base64Encoded: bool = Field(description="Whether the data is base64 encoded")
register_model_labels(
"ContentMetadata",
{"en": "Content Metadata", "fr": "Métadonnées du contenu"},
{
"size": {"en": "Size", "fr": "Taille"},
"pages": {"en": "Pages", "fr": "Pages"},
"error": {"en": "Error", "fr": "Erreur"},
"width": {"en": "Width", "fr": "Largeur"},
"height": {"en": "Height", "fr": "Hauteur"},
"colorMode": {"en": "Color Mode", "fr": "Mode de couleur"},
"fps": {"en": "FPS", "fr": "IPS"},
"durationSec": {"en": "Duration", "fr": "Durée"},
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
"base64Encoded": {"en": "Base64 Encoded", "fr": "Encodé en Base64"},
},
)
class ContentItem(BaseModel, ModelMixin):
label: str = Field(description="Content label")
data: str = Field(description="Extracted text content")
metadata: ContentMetadata = Field(description="Content metadata")
register_model_labels(
"ContentItem",
{"en": "Content Item", "fr": "Élément de contenu"},
{
"label": {"en": "Label", "fr": "Étiquette"},
"data": {"en": "Data", "fr": "Données"},
"metadata": {"en": "Metadata", "fr": "Métadonnées"},
},
)
class ExtractedContent(BaseModel, ModelMixin):
id: str = Field(description="Reference to source ChatDocument")
contents: List[ContentItem] = Field(default_factory=list, description="List of content items")
register_model_labels(
"ExtractedContent",
{"en": "Extracted Content", "fr": "Contenu extrait"},
{
"id": {"en": "Object ID", "fr": "ID de l'objet"},
"contents": {"en": "Contents", "fr": "Contenus"},
},
)
class ChatMessage(BaseModel, ModelMixin):
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
workflowId: str = Field(description="Foreign key to workflow")
parentMessageId: Optional[str] = Field(None, description="Parent message ID for threading")
documents: List[ChatDocument] = Field(default_factory=list, description="Associated documents")
documentsLabel: Optional[str] = Field(None, description="Label for the set of documents")
message: Optional[str] = Field(None, description="Message content")
role: str = Field(description="Role of the message sender")
status: str = Field(description="Status of the message (first, step, last)")
sequenceNr: int = Field(description="Sequence number of the message (set automatically)")
publishedAt: float = Field(default_factory=get_utc_timestamp, description="When the message was published (UTC timestamp in seconds)")
stats: Optional[ChatStat] = Field(None, description="Statistics for this message")
success: Optional[bool] = Field(None, description="Whether the message processing was successful")
actionId: Optional[str] = Field(None, description="ID of the action that produced this message")
actionMethod: Optional[str] = Field(None, description="Method of the action that produced this message")
actionName: Optional[str] = Field(None, description="Name of the action that produced this message")
roundNumber: Optional[int] = Field(None, description="Round number in workflow")
taskNumber: Optional[int] = Field(None, description="Task number within round")
actionNumber: Optional[int] = Field(None, description="Action number within task")
taskProgress: Optional[str] = Field(None, description="Task progress status: pending, running, success, fail, retry")
actionProgress: Optional[str] = Field(None, description="Action progress status: pending, running, success, fail")
register_model_labels(
"ChatMessage",
{"en": "Chat Message", "fr": "Message de chat"},
{
"id": {"en": "ID", "fr": "ID"},
"workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
"parentMessageId": {"en": "Parent Message ID", "fr": "ID du message parent"},
"documents": {"en": "Documents", "fr": "Documents"},
"documentsLabel": {"en": "Documents Label", "fr": "Label des documents"},
"message": {"en": "Message", "fr": "Message"},
"role": {"en": "Role", "fr": "Rôle"},
"status": {"en": "Status", "fr": "Statut"},
"sequenceNr": {"en": "Sequence Number", "fr": "Numéro de séquence"},
"publishedAt": {"en": "Published At", "fr": "Publié le"},
"stats": {"en": "Statistics", "fr": "Statistiques"},
"success": {"en": "Success", "fr": "Succès"},
"actionId": {"en": "Action ID", "fr": "ID de l'action"},
"actionMethod": {"en": "Action Method", "fr": "Méthode de l'action"},
"actionName": {"en": "Action Name", "fr": "Nom de l'action"},
"roundNumber": {"en": "Round Number", "fr": "Numéro de tour"},
"taskNumber": {"en": "Task Number", "fr": "Numéro de tâche"},
"actionNumber": {"en": "Action Number", "fr": "Numéro d'action"},
"taskProgress": {"en": "Task Progress", "fr": "Progression de la tâche"},
"actionProgress": {"en": "Action Progress", "fr": "Progression de l'action"},
},
)
class ChatWorkflow(BaseModel, ModelMixin):
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", frontend_type="text", frontend_readonly=True, frontend_required=False)
mandateId: str = Field(description="ID of the mandate this workflow belongs to", frontend_type="text", frontend_readonly=True, frontend_required=False)
status: str = Field(description="Current status of the workflow", frontend_type="select", frontend_readonly=False, frontend_required=False, frontend_options=[
{"value": "running", "label": {"en": "Running", "fr": "En cours"}},
{"value": "completed", "label": {"en": "Completed", "fr": "Terminé"}},
{"value": "stopped", "label": {"en": "Stopped", "fr": "Arrêté"}},
{"value": "error", "label": {"en": "Error", "fr": "Erreur"}},
])
name: Optional[str] = Field(None, description="Name of the workflow", frontend_type="text", frontend_readonly=False, frontend_required=True)
currentRound: int = Field(description="Current round number", frontend_type="integer", frontend_readonly=True, frontend_required=False)
currentTask: int = Field(default=0, description="Current task number", frontend_type="integer", frontend_readonly=True, frontend_required=False)
currentAction: int = Field(default=0, description="Current action number", frontend_type="integer", frontend_readonly=True, frontend_required=False)
totalTasks: int = Field(default=0, description="Total number of tasks in the workflow", frontend_type="integer", frontend_readonly=True, frontend_required=False)
totalActions: int = Field(default=0, description="Total number of actions in the workflow", frontend_type="integer", frontend_readonly=True, frontend_required=False)
lastActivity: float = Field(default_factory=get_utc_timestamp, description="Timestamp of last activity (UTC timestamp in seconds)", frontend_type="timestamp", frontend_readonly=True, frontend_required=False)
startedAt: float = Field(default_factory=get_utc_timestamp, description="When the workflow started (UTC timestamp in seconds)", frontend_type="timestamp", frontend_readonly=True, frontend_required=False)
logs: List[ChatLog] = Field(default_factory=list, description="Workflow logs", frontend_type="text", frontend_readonly=True, frontend_required=False)
messages: List[ChatMessage] = Field(default_factory=list, description="Messages in the workflow", frontend_type="text", frontend_readonly=True, frontend_required=False)
stats: Optional[ChatStat] = Field(None, description="Workflow statistics", frontend_type="text", frontend_readonly=True, frontend_required=False)
tasks: list = Field(default_factory=list, description="List of tasks in the workflow", frontend_type="text", frontend_readonly=True, frontend_required=False)
workflowMode: str = Field(default="Actionplan", description="Workflow mode selector", frontend_type="select", frontend_readonly=False, frontend_required=False, frontend_options=[
{"value": "Actionplan", "label": {"en": "Action Plan", "fr": "Plan d'actions"}},
{"value": "React", "label": {"en": "React", "fr": "Réactif"}},
])
maxSteps: int = Field(default=5, description="Maximum number of iterations in react mode", frontend_type="integer", frontend_readonly=False, frontend_required=False)
register_model_labels(
"ChatWorkflow",
{"en": "Chat Workflow", "fr": "Flux de travail de chat"},
{
"id": {"en": "ID", "fr": "ID"},
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
"status": {"en": "Status", "fr": "Statut"},
"name": {"en": "Name", "fr": "Nom"},
"currentRound": {"en": "Current Round", "fr": "Tour actuel"},
"currentTask": {"en": "Current Task", "fr": "Tâche actuelle"},
"currentAction": {"en": "Current Action", "fr": "Action actuelle"},
"totalTasks": {"en": "Total Tasks", "fr": "Total des tâches"},
"totalActions": {"en": "Total Actions", "fr": "Total des actions"},
"lastActivity": {"en": "Last Activity", "fr": "Dernière activité"},
"startedAt": {"en": "Started At", "fr": "Démarré le"},
"logs": {"en": "Logs", "fr": "Journaux"},
"messages": {"en": "Messages", "fr": "Messages"},
"stats": {"en": "Statistics", "fr": "Statistiques"},
"tasks": {"en": "Tasks", "fr": "Tâches"},
"workflowMode": {"en": "Workflow Mode", "fr": "Mode de workflow"},
"maxSteps": {"en": "Max Steps", "fr": "Étapes max"},
},
)

View file

@ -0,0 +1,84 @@
"""File-related datamodels: FileItem, FilePreview, FileData."""
from typing import Dict, Any, Optional, Union
from pydantic import BaseModel, Field
from modules.shared.attributeUtils import register_model_labels, ModelMixin
from modules.shared.timezoneUtils import get_utc_timestamp
import uuid
import base64
class FileItem(BaseModel, ModelMixin):
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", frontend_type="text", frontend_readonly=True, frontend_required=False)
mandateId: str = Field(description="ID of the mandate this file belongs to", frontend_type="text", frontend_readonly=True, frontend_required=False)
fileName: str = Field(description="Name of the file", frontend_type="text", frontend_readonly=False, frontend_required=True)
mimeType: str = Field(description="MIME type of the file", frontend_type="text", frontend_readonly=True, frontend_required=False)
fileHash: str = Field(description="Hash of the file", frontend_type="text", frontend_readonly=True, frontend_required=False)
fileSize: int = Field(description="Size of the file in bytes", frontend_type="integer", frontend_readonly=True, frontend_required=False)
creationDate: float = Field(default_factory=get_utc_timestamp, description="Date when the file was created (UTC timestamp in seconds)", frontend_type="timestamp", frontend_readonly=True, frontend_required=False)
def to_dict(self) -> Dict[str, Any]:
return super().to_dict()
register_model_labels(
"FileItem",
{"en": "File Item", "fr": "Élément de fichier"},
{
"id": {"en": "ID", "fr": "ID"},
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
"fileName": {"en": "fileName", "fr": "Nom de fichier"},
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
"fileHash": {"en": "File Hash", "fr": "Hash du fichier"},
"fileSize": {"en": "File Size", "fr": "Taille du fichier"},
"creationDate": {"en": "Creation Date", "fr": "Date de création"},
},
)
class FilePreview(BaseModel, ModelMixin):
content: Union[str, bytes] = Field(description="File content (text or binary)")
mimeType: str = Field(description="MIME type of the file")
fileName: str = Field(description="Original fileName")
isText: bool = Field(description="Whether the content is text (True) or binary (False)")
encoding: Optional[str] = Field(None, description="Text encoding if content is text")
size: int = Field(description="Size of the content in bytes")
def to_dict(self) -> Dict[str, Any]:
data = super().to_dict()
if isinstance(data.get("content"), bytes):
data["content"] = base64.b64encode(data["content"]).decode("utf-8")
return data
register_model_labels(
"FilePreview",
{"en": "File Preview", "fr": "Aperçu du fichier"},
{
"content": {"en": "Content", "fr": "Contenu"},
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
"fileName": {"en": "fileName", "fr": "Nom de fichier"},
"isText": {"en": "Is Text", "fr": "Est du texte"},
"encoding": {"en": "Encoding", "fr": "Encodage"},
"size": {"en": "Size", "fr": "Taille"},
},
)
class FileData(BaseModel, ModelMixin):
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
data: str = Field(description="File data content")
base64Encoded: bool = Field(description="Whether the data is base64 encoded")
register_model_labels(
"FileData",
{"en": "File Data", "fr": "Données de fichier"},
{
"id": {"en": "ID", "fr": "ID"},
"data": {"en": "Data", "fr": "Données"},
"base64Encoded": {"en": "Base64 Encoded", "fr": "Encodé en Base64"},
},
)

View file

@ -0,0 +1,56 @@
"""Neutralizer models: DataNeutraliserConfig and DataNeutralizerAttributes."""
import uuid
from typing import Optional
from pydantic import BaseModel, Field
from modules.shared.attributeUtils import register_model_labels, ModelMixin
class DataNeutraliserConfig(BaseModel, ModelMixin):
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the configuration", frontend_type="text", frontend_readonly=True, frontend_required=False)
mandateId: str = Field(description="ID of the mandate this configuration belongs to", frontend_type="text", frontend_readonly=True, frontend_required=True)
userId: str = Field(description="ID of the user who created this configuration", frontend_type="text", frontend_readonly=True, frontend_required=True)
enabled: bool = Field(default=True, description="Whether data neutralization is enabled", frontend_type="checkbox", frontend_readonly=False, frontend_required=False)
namesToParse: str = Field(default="", description="Multiline list of names to parse for neutralization", frontend_type="textarea", frontend_readonly=False, frontend_required=False)
sharepointSourcePath: str = Field(default="", description="SharePoint path to read files for neutralization", frontend_type="text", frontend_readonly=False, frontend_required=False)
sharepointTargetPath: str = Field(default="", description="SharePoint path to store neutralized files", frontend_type="text", frontend_readonly=False, frontend_required=False)
register_model_labels(
"DataNeutraliserConfig",
{"en": "Data Neutralization Config", "fr": "Configuration de neutralisation des données"},
{
"id": {"en": "ID", "fr": "ID"},
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
"userId": {"en": "User ID", "fr": "ID utilisateur"},
"enabled": {"en": "Enabled", "fr": "Activé"},
"namesToParse": {"en": "Names to Parse", "fr": "Noms à analyser"},
"sharepointSourcePath": {"en": "Source Path", "fr": "Chemin source"},
"sharepointTargetPath": {"en": "Target Path", "fr": "Chemin cible"},
},
)
class DataNeutralizerAttributes(BaseModel, ModelMixin):
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the attribute mapping (used as UID in neutralized files)", frontend_type="text", frontend_readonly=True, frontend_required=False)
mandateId: str = Field(description="ID of the mandate this attribute belongs to", frontend_type="text", frontend_readonly=True, frontend_required=True)
userId: str = Field(description="ID of the user who created this attribute", frontend_type="text", frontend_readonly=True, frontend_required=True)
originalText: str = Field(description="Original text that was neutralized", frontend_type="text", frontend_readonly=True, frontend_required=True)
fileId: Optional[str] = Field(default=None, description="ID of the file this attribute belongs to", frontend_type="text", frontend_readonly=True, frontend_required=False)
patternType: str = Field(description="Type of pattern that matched (email, phone, name, etc.)", frontend_type="text", frontend_readonly=True, frontend_required=True)
register_model_labels(
"DataNeutralizerAttributes",
{"en": "Neutralized Data Attribute", "fr": "Attribut de données neutralisées"},
{
"id": {"en": "ID", "fr": "ID"},
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
"userId": {"en": "User ID", "fr": "ID utilisateur"},
"originalText": {"en": "Original Text", "fr": "Texte original"},
"fileId": {"en": "File ID", "fr": "ID de fichier"},
"patternType": {"en": "Pattern Type", "fr": "Type de modèle"},
},
)

View file

@ -0,0 +1,87 @@
"""Security models: Token and AuthEvent."""
from typing import Optional
from pydantic import BaseModel, Field
from modules.shared.attributeUtils import register_model_labels, ModelMixin
from modules.shared.timezoneUtils import get_utc_timestamp
from .datamodelUam import AuthAuthority
from enum import Enum
import uuid
class TokenStatus(str, Enum):
ACTIVE = "active"
REVOKED = "revoked"
class Token(BaseModel, ModelMixin):
id: Optional[str] = None
userId: str
authority: AuthAuthority
connectionId: Optional[str] = Field(None, description="ID of the connection this token belongs to")
tokenAccess: str
tokenType: str = "bearer"
expiresAt: float = Field(description="When the token expires (UTC timestamp in seconds)")
tokenRefresh: Optional[str] = None
createdAt: Optional[float] = Field(None, description="When the token was created (UTC timestamp in seconds)")
status: TokenStatus = Field(default=TokenStatus.ACTIVE, description="Token status: active/revoked")
revokedAt: Optional[float] = Field(None, description="When the token was revoked (UTC timestamp in seconds)")
revokedBy: Optional[str] = Field(None, description="User ID who revoked the token (admin/self)")
reason: Optional[str] = Field(None, description="Optional revocation reason")
sessionId: Optional[str] = Field(None, description="Logical session grouping for logout revocation")
mandateId: Optional[str] = Field(None, description="Mandate ID for tenant scoping of the token")
class Config:
use_enum_values = True
register_model_labels(
"Token",
{"en": "Token", "fr": "Jeton"},
{
"id": {"en": "ID", "fr": "ID"},
"userId": {"en": "User ID", "fr": "ID utilisateur"},
"authority": {"en": "Authority", "fr": "Autorité"},
"connectionId": {"en": "Connection ID", "fr": "ID de connexion"},
"tokenAccess": {"en": "Access Token", "fr": "Jeton d'accès"},
"tokenType": {"en": "Token Type", "fr": "Type de jeton"},
"expiresAt": {"en": "Expires At", "fr": "Expire le"},
"tokenRefresh": {"en": "Refresh Token", "fr": "Jeton de rafraîchissement"},
"createdAt": {"en": "Created At", "fr": "Créé le"},
"status": {"en": "Status", "fr": "Statut"},
"revokedAt": {"en": "Revoked At", "fr": "Révoqué le"},
"revokedBy": {"en": "Revoked By", "fr": "Révoqué par"},
"reason": {"en": "Reason", "fr": "Raison"},
"sessionId": {"en": "Session ID", "fr": "ID de session"},
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
},
)
class AuthEvent(BaseModel, ModelMixin):
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the auth event", frontend_type="text", frontend_readonly=True, frontend_required=False)
userId: str = Field(description="ID of the user this event belongs to", frontend_type="text", frontend_readonly=True, frontend_required=True)
eventType: str = Field(description="Type of authentication event (e.g., 'login', 'logout', 'token_refresh')", frontend_type="text", frontend_readonly=True, frontend_required=True)
timestamp: float = Field(default_factory=get_utc_timestamp, description="Unix timestamp when the event occurred", frontend_type="datetime", frontend_readonly=True, frontend_required=True)
ipAddress: Optional[str] = Field(default=None, description="IP address from which the event originated", frontend_type="text", frontend_readonly=True, frontend_required=False)
userAgent: Optional[str] = Field(default=None, description="User agent string from the request", frontend_type="text", frontend_readonly=True, frontend_required=False)
success: bool = Field(default=True, description="Whether the authentication event was successful", frontend_type="boolean", frontend_readonly=True, frontend_required=True)
details: Optional[str] = Field(default=None, description="Additional details about the event", frontend_type="text", frontend_readonly=True, frontend_required=False)
register_model_labels(
"AuthEvent",
{"en": "Authentication Event", "fr": "Événement d'authentification"},
{
"id": {"en": "ID", "fr": "ID"},
"userId": {"en": "User ID", "fr": "ID utilisateur"},
"eventType": {"en": "Event Type", "fr": "Type d'événement"},
"timestamp": {"en": "Timestamp", "fr": "Horodatage"},
"ipAddress": {"en": "IP Address", "fr": "Adresse IP"},
"userAgent": {"en": "User Agent", "fr": "Agent utilisateur"},
"success": {"en": "Success", "fr": "Succès"},
"details": {"en": "Details", "fr": "Détails"},
},
)

View file

@ -1,6 +1,6 @@
"""Base class for ticket classes.""" """Ticket datamodels used across Jira/ClickUp connectors."""
from typing import Any, Dict from typing import Optional
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
@ -10,17 +10,14 @@ class TicketFieldAttribute(BaseModel):
field: str = Field(description="Ticket field ID/key") field: str = Field(description="Ticket field ID/key")
class Task(BaseModel):
# A very flexible approach for now. Might want to be more strict in the future.
data: Dict[str, Any] = Field(default_factory=dict, description="Task data")
class TicketBase(ABC): class TicketBase(ABC):
@abstractmethod @abstractmethod
async def read_attributes(self) -> list[TicketFieldAttribute]: ... async def read_attributes(self) -> list[TicketFieldAttribute]: ...
@abstractmethod @abstractmethod
async def read_tasks(self, limit: int = 0) -> list[Task]: ... async def read_tasks(self, *, limit: int = 0) -> list[dict]: ...
@abstractmethod @abstractmethod
async def write_tasks(self, tasklist: list[Task]) -> None: ... async def write_tasks(self, tasklist: list[dict]) -> None: ...

View file

@ -0,0 +1,154 @@
"""UAM models: User, Mandate, UserConnection."""
import uuid
from typing import Optional
from enum import Enum
from pydantic import BaseModel, Field, EmailStr
from modules.shared.attributeUtils import register_model_labels, ModelMixin
from modules.shared.timezoneUtils import get_utc_timestamp
class AuthAuthority(str, Enum):
LOCAL = "local"
GOOGLE = "google"
MSFT = "msft"
class UserPrivilege(str, Enum):
SYSADMIN = "sysadmin"
ADMIN = "admin"
USER = "user"
class ConnectionStatus(str, Enum):
ACTIVE = "active"
EXPIRED = "expired"
REVOKED = "revoked"
PENDING = "pending"
class Mandate(BaseModel, ModelMixin):
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the mandate", frontend_type="text", frontend_readonly=True, frontend_required=False)
name: str = Field(description="Name of the mandate", frontend_type="text", frontend_readonly=False, frontend_required=True)
language: str = Field(default="en", description="Default language of the mandate", frontend_type="select", frontend_readonly=False, frontend_required=True, frontend_options=[
{"value": "de", "label": {"en": "Deutsch", "fr": "Allemand"}},
{"value": "en", "label": {"en": "English", "fr": "Anglais"}},
{"value": "fr", "label": {"en": "Français", "fr": "Français"}},
{"value": "it", "label": {"en": "Italiano", "fr": "Italien"}},
])
enabled: bool = Field(default=True, description="Indicates whether the mandate is enabled", frontend_type="checkbox", frontend_readonly=False, frontend_required=False)
register_model_labels(
"Mandate",
{"en": "Mandate", "fr": "Mandat"},
{
"id": {"en": "ID", "fr": "ID"},
"name": {"en": "Name", "fr": "Nom"},
"language": {"en": "Language", "fr": "Langue"},
"enabled": {"en": "Enabled", "fr": "Activé"},
},
)
class UserConnection(BaseModel, ModelMixin):
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the connection", frontend_type="text", frontend_readonly=True, frontend_required=False)
userId: str = Field(description="ID of the user this connection belongs to", frontend_type="text", frontend_readonly=True, frontend_required=False)
authority: AuthAuthority = Field(description="Authentication authority", frontend_type="select", frontend_readonly=True, frontend_required=False, frontend_options=[
{"value": "local", "label": {"en": "Local", "fr": "Local"}},
{"value": "google", "label": {"en": "Google", "fr": "Google"}},
{"value": "msft", "label": {"en": "Microsoft", "fr": "Microsoft"}},
])
externalId: str = Field(description="User ID in the external system", frontend_type="text", frontend_readonly=True, frontend_required=False)
externalUsername: str = Field(description="Username in the external system", frontend_type="text", frontend_readonly=False, frontend_required=False)
externalEmail: Optional[EmailStr] = Field(None, description="Email in the external system", frontend_type="email", frontend_readonly=False, frontend_required=False)
status: ConnectionStatus = Field(default=ConnectionStatus.ACTIVE, description="Connection status", frontend_type="select", frontend_readonly=False, frontend_required=False, frontend_options=[
{"value": "active", "label": {"en": "Active", "fr": "Actif"}},
{"value": "inactive", "label": {"en": "Inactive", "fr": "Inactif"}},
{"value": "expired", "label": {"en": "Expired", "fr": "Expiré"}},
{"value": "pending", "label": {"en": "Pending", "fr": "En attente"}},
])
connectedAt: float = Field(default_factory=get_utc_timestamp, description="When the connection was established (UTC timestamp in seconds)", frontend_type="timestamp", frontend_readonly=True, frontend_required=False)
lastChecked: float = Field(default_factory=get_utc_timestamp, description="When the connection was last verified (UTC timestamp in seconds)", frontend_type="timestamp", frontend_readonly=True, frontend_required=False)
expiresAt: Optional[float] = Field(None, description="When the connection expires (UTC timestamp in seconds)", frontend_type="timestamp", frontend_readonly=True, frontend_required=False)
tokenStatus: Optional[str] = Field(None, description="Current token status: active, expired, none", frontend_type="select", frontend_readonly=True, frontend_required=False, frontend_options=[
{"value": "active", "label": {"en": "Active", "fr": "Actif"}},
{"value": "expired", "label": {"en": "Expired", "fr": "Expiré"}},
{"value": "none", "label": {"en": "None", "fr": "Aucun"}},
])
tokenExpiresAt: Optional[float] = Field(None, description="When the current token expires (UTC timestamp in seconds)", frontend_type="timestamp", frontend_readonly=True, frontend_required=False)
register_model_labels(
"UserConnection",
{"en": "User Connection", "fr": "Connexion utilisateur"},
{
"id": {"en": "ID", "fr": "ID"},
"userId": {"en": "User ID", "fr": "ID utilisateur"},
"authority": {"en": "Authority", "fr": "Autorité"},
"externalId": {"en": "External ID", "fr": "ID externe"},
"externalUsername": {"en": "External Username", "fr": "Nom d'utilisateur externe"},
"externalEmail": {"en": "External Email", "fr": "Email externe"},
"status": {"en": "Status", "fr": "Statut"},
"connectedAt": {"en": "Connected At", "fr": "Connecté le"},
"lastChecked": {"en": "Last Checked", "fr": "Dernière vérification"},
"expiresAt": {"en": "Expires At", "fr": "Expire le"},
"tokenStatus": {"en": "Connection Status", "fr": "Statut de connexion"},
"tokenExpiresAt": {"en": "Expires At", "fr": "Expire le"},
},
)
class User(BaseModel, ModelMixin):
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the user", frontend_type="text", frontend_readonly=True, frontend_required=False)
username: str = Field(description="Username for login", frontend_type="text", frontend_readonly=False, frontend_required=True)
email: Optional[EmailStr] = Field(None, description="Email address of the user", frontend_type="email", frontend_readonly=False, frontend_required=True)
fullName: Optional[str] = Field(None, description="Full name of the user", frontend_type="text", frontend_readonly=False, frontend_required=False)
language: str = Field(default="en", description="Preferred language of the user", frontend_type="select", frontend_readonly=False, frontend_required=True, frontend_options=[
{"value": "de", "label": {"en": "Deutsch", "fr": "Allemand"}},
{"value": "en", "label": {"en": "English", "fr": "Anglais"}},
{"value": "fr", "label": {"en": "Français", "fr": "Français"}},
{"value": "it", "label": {"en": "Italiano", "fr": "Italien"}},
])
enabled: bool = Field(default=True, description="Indicates whether the user is enabled", frontend_type="checkbox", frontend_readonly=False, frontend_required=False)
privilege: UserPrivilege = Field(default=UserPrivilege.USER, description="Permission level", frontend_type="select", frontend_readonly=False, frontend_required=True, frontend_options=[
{"value": "user", "label": {"en": "User", "fr": "Utilisateur"}},
{"value": "admin", "label": {"en": "Admin", "fr": "Administrateur"}},
{"value": "sysadmin", "label": {"en": "SysAdmin", "fr": "Administrateur système"}},
])
authenticationAuthority: AuthAuthority = Field(default=AuthAuthority.LOCAL, description="Primary authentication authority", frontend_type="select", frontend_readonly=True, frontend_required=False, frontend_options=[
{"value": "local", "label": {"en": "Local", "fr": "Local"}},
{"value": "google", "label": {"en": "Google", "fr": "Google"}},
{"value": "msft", "label": {"en": "Microsoft", "fr": "Microsoft"}},
])
mandateId: Optional[str] = Field(None, description="ID of the mandate this user belongs to", frontend_type="text", frontend_readonly=True, frontend_required=False)
register_model_labels(
"User",
{"en": "User", "fr": "Utilisateur"},
{
"id": {"en": "ID", "fr": "ID"},
"username": {"en": "Username", "fr": "Nom d'utilisateur"},
"email": {"en": "Email", "fr": "Email"},
"fullName": {"en": "Full Name", "fr": "Nom complet"},
"language": {"en": "Language", "fr": "Langue"},
"enabled": {"en": "Enabled", "fr": "Activé"},
"privilege": {"en": "Privilege", "fr": "Privilège"},
"authenticationAuthority": {"en": "Auth Authority", "fr": "Autorité d'authentification"},
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
},
)
class UserInDB(User):
hashedPassword: Optional[str] = Field(None, description="Hash of the user password")
register_model_labels(
"UserInDB",
{"en": "User Access", "fr": "Accès de l'utilisateur"},
{"hashedPassword": {"en": "Password hash", "fr": "Hachage de mot de passe"}},
)

View file

@ -0,0 +1,26 @@
"""Utility datamodels: Prompt."""
from pydantic import BaseModel, Field
from modules.shared.attributeUtils import register_model_labels, ModelMixin
import uuid
class Prompt(BaseModel, ModelMixin):
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", frontend_type="text", frontend_readonly=True, frontend_required=False)
mandateId: str = Field(description="ID of the mandate this prompt belongs to", frontend_type="text", frontend_readonly=True, frontend_required=False)
content: str = Field(description="Content of the prompt", frontend_type="textarea", frontend_readonly=False, frontend_required=True)
name: str = Field(description="Name of the prompt", frontend_type="text", frontend_readonly=False, frontend_required=True)
register_model_labels(
"Prompt",
{"en": "Prompt", "fr": "Invite"},
{
"id": {"en": "ID", "fr": "ID"},
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
"content": {"en": "Content", "fr": "Contenu"},
"name": {"en": "Name", "fr": "Nom"},
},
)

View file

@ -0,0 +1,43 @@
"""Voice settings datamodel."""
from typing import Dict, Any, Optional
from pydantic import BaseModel, Field
from modules.shared.attributeUtils import register_model_labels, ModelMixin
from modules.shared.timezoneUtils import get_utc_timestamp
import uuid
class VoiceSettings(BaseModel, ModelMixin):
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", frontend_type="text", frontend_readonly=True, frontend_required=False)
userId: str = Field(description="ID of the user these settings belong to", frontend_type="text", frontend_readonly=True, frontend_required=True)
mandateId: str = Field(description="ID of the mandate these settings belong to", frontend_type="text", frontend_readonly=True, frontend_required=False)
sttLanguage: str = Field(default="de-DE", description="Speech-to-Text language", frontend_type="select", frontend_readonly=False, frontend_required=True)
ttsLanguage: str = Field(default="de-DE", description="Text-to-Speech language", frontend_type="select", frontend_readonly=False, frontend_required=True)
ttsVoice: str = Field(default="de-DE-KatjaNeural", description="Text-to-Speech voice", frontend_type="select", frontend_readonly=False, frontend_required=True)
translationEnabled: bool = Field(default=True, description="Whether translation is enabled", frontend_type="checkbox", frontend_readonly=False, frontend_required=False)
targetLanguage: str = Field(default="en-US", description="Target language for translation", frontend_type="select", frontend_readonly=False, frontend_required=False)
creationDate: float = Field(default_factory=get_utc_timestamp, description="Date when the settings were created (UTC timestamp in seconds)", frontend_type="timestamp", frontend_readonly=True, frontend_required=False)
lastModified: float = Field(default_factory=get_utc_timestamp, description="Date when the settings were last modified (UTC timestamp in seconds)", frontend_type="timestamp", frontend_readonly=True, frontend_required=False)
def to_dict(self) -> Dict[str, Any]:
return super().to_dict()
register_model_labels(
"VoiceSettings",
{"en": "Voice Settings", "fr": "Paramètres vocaux"},
{
"id": {"en": "ID", "fr": "ID"},
"userId": {"en": "User ID", "fr": "ID utilisateur"},
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
"sttLanguage": {"en": "STT Language", "fr": "Langue STT"},
"ttsLanguage": {"en": "TTS Language", "fr": "Langue TTS"},
"ttsVoice": {"en": "TTS Voice", "fr": "Voix TTS"},
"translationEnabled": {"en": "Translation Enabled", "fr": "Traduction activée"},
"targetLanguage": {"en": "Target Language", "fr": "Langue cible"},
"creationDate": {"en": "Creation Date", "fr": "Date de création"},
"lastModified": {"en": "Last Modified", "fr": "Dernière modification"},
},
)

View file

@ -1,36 +1,20 @@
"""Base class for web classes.""" """Web-related modules.datamodels (search, crawl, scrape)."""
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from modules.interfaces.interfaceChatModel import ActionDocument, ActionResult
from pydantic import BaseModel, Field, HttpUrl from pydantic import BaseModel, Field, HttpUrl
from typing import List, Optional, Literal from typing import List, Optional, Literal
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.datamodels.datamodelWorkflow import ActionDocument, ActionResult
# Configuration loading functions WEB_SEARCH_MAX_QUERY_LENGTH: int = int(APP_CONFIG.get("Web_Search_MAX_QUERY_LENGTH", "400"))
def get_web_search_max_query_length() -> int: WEB_SEARCH_MAX_RESULTS: int = int(APP_CONFIG.get("Web_Search_MAX_RESULTS", "20"))
"""Get maximum query length from configuration""" WEB_SEARCH_MIN_RESULTS: int = int(APP_CONFIG.get("Web_Search_MIN_RESULTS", "1"))
return int(APP_CONFIG.get("Web_Search_MAX_QUERY_LENGTH", "400"))
def get_web_search_max_results() -> int:
"""Get maximum search results from configuration"""
return int(APP_CONFIG.get("Web_Search_MAX_RESULTS", "20"))
def get_web_search_min_results() -> int:
"""Get minimum search results from configuration"""
return int(APP_CONFIG.get("Web_Search_MIN_RESULTS", "1"))
# --- Web search ---
# query -> list of URLs
class WebSearchRequest(BaseModel): class WebSearchRequest(BaseModel):
query: str = Field(min_length=1, max_length=get_web_search_max_query_length()) query: str = Field(min_length=1, max_length=WEB_SEARCH_MAX_QUERY_LENGTH)
max_results: int = Field(ge=get_web_search_min_results(), le=get_web_search_max_results()) max_results: int = Field(ge=WEB_SEARCH_MIN_RESULTS, le=WEB_SEARCH_MAX_RESULTS)
# Tavily tuning options # Tavily tuning options
search_depth: Optional[Literal["basic", "advanced"]] = Field(default=None) search_depth: Optional[Literal["basic", "advanced"]] = Field(default=None)
time_range: Optional[Literal["d", "w", "m", "y"]] = Field( time_range: Optional[Literal["d", "w", "m", "y"]] = Field(
@ -52,10 +36,11 @@ class WebSearchResultItem(BaseModel):
class WebSearchDocumentData(BaseModel): class WebSearchDocumentData(BaseModel):
"""Complete search results document""" """Complete search (and scrape) results document"""
query: str = Field(min_length=1, max_length=get_web_search_max_query_length()) query: str = Field(min_length=1, max_length=WEB_SEARCH_MAX_QUERY_LENGTH)
results: List[WebSearchResultItem] # Allow both WebSearchResultItem and WebScrapeResultItem to be stored here
results: List[object]
total_count: int total_count: int
@ -74,8 +59,6 @@ class WebSearchBase(ABC):
# --- Web crawl --- # --- Web crawl ---
# list of URLs -> list of extracted HTML content
class WebCrawlRequest(BaseModel): class WebCrawlRequest(BaseModel):
urls: List[HttpUrl] urls: List[HttpUrl]
@ -116,12 +99,10 @@ class WebCrawlBase(ABC):
# --- Web scrape --- # --- Web scrape ---
# scrape -> list of extracted text; combines web search and crawl in one step
class WebScrapeRequest(BaseModel): class WebScrapeRequest(BaseModel):
query: str = Field(min_length=1, max_length=get_web_search_max_query_length()) query: str = Field(min_length=1, max_length=WEB_SEARCH_MAX_QUERY_LENGTH)
max_results: int = Field(ge=get_web_search_min_results(), le=get_web_search_max_results()) max_results: int = Field(ge=WEB_SEARCH_MIN_RESULTS, le=WEB_SEARCH_MAX_RESULTS)
# Pass-through search options # Pass-through search options
search_depth: Optional[Literal["basic", "advanced"]] = Field(default=None) search_depth: Optional[Literal["basic", "advanced"]] = Field(default=None)
time_range: Optional[Literal["d", "w", "m", "y"]] = Field(default=None) time_range: Optional[Literal["d", "w", "m", "y"]] = Field(default=None)
@ -143,16 +124,8 @@ class WebScrapeResultItem(BaseModel):
content: str content: str
class WebScrapeDocumentData(BaseModel):
"""Complete scrape results document"""
query: str = Field(min_length=1, max_length=get_web_search_max_query_length())
results: List[WebScrapeResultItem]
total_count: int
class WebScrapeActionDocument(ActionDocument): class WebScrapeActionDocument(ActionDocument):
documentData: WebScrapeDocumentData = Field( documentData: WebSearchDocumentData = Field(
description="The data extracted from scraped URLs" description="The data extracted from scraped URLs"
) )
@ -164,3 +137,5 @@ class WebScrapeActionResult(ActionResult):
class WebScrapeBase(ABC): class WebScrapeBase(ABC):
@abstractmethod @abstractmethod
async def scrape(self, request: WebScrapeRequest) -> WebScrapeActionResult: ... async def scrape(self, request: WebScrapeRequest) -> WebScrapeActionResult: ...

View file

@ -0,0 +1,446 @@
"""Workflow-related base datamodels and step/task structures."""
from typing import List, Dict, Any, Optional
from pydantic import BaseModel, Field
from modules.shared.attributeUtils import register_model_labels, ModelMixin
class ActionDocument(BaseModel, ModelMixin):
"""Clear document structure for action results"""
documentName: str = Field(description="Name of the document")
documentData: Any = Field(description="Content/data of the document")
mimeType: str = Field(description="MIME type of the document")
register_model_labels(
"ActionDocument",
{"en": "Action Document", "fr": "Document d'action"},
{
"documentName": {"en": "Document Name", "fr": "Nom du document"},
"documentData": {"en": "Document Data", "fr": "Données du document"},
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
},
)
class ActionResult(BaseModel, ModelMixin):
"""Clean action result with documents as primary output
IMPORTANT: Action methods should NOT set resultLabel in their return value.
The resultLabel is managed by the action handler using the action's execResultLabel
from the action plan. This ensures consistent document routing throughout the workflow.
"""
success: bool = Field(description="Whether execution succeeded")
error: Optional[str] = Field(None, description="Error message if failed")
documents: List[ActionDocument] = Field(default_factory=list, description="Document outputs")
resultLabel: Optional[str] = Field(None, description="Label for document routing (set by action handler, not by action methods)")
@classmethod
def isSuccess(cls, documents: List[ActionDocument] = None) -> "ActionResult":
return cls(success=True, documents=documents or [])
@classmethod
def isFailure(cls, error: str, documents: List[ActionDocument] = None) -> "ActionResult":
return cls(success=False, documents=documents or [], error=error)
register_model_labels(
"ActionResult",
{"en": "Action Result", "fr": "Résultat de l'action"},
{
"success": {"en": "Success", "fr": "Succès"},
"error": {"en": "Error", "fr": "Erreur"},
"documents": {"en": "Documents", "fr": "Documents"},
"resultLabel": {"en": "Result Label", "fr": "Étiquette du résultat"},
},
)
# ===== Additional workflow models migrated from interfaceChatModel =====
class ActionSelection(BaseModel, ModelMixin):
method: str = Field(description="Method to execute (e.g., web, document, ai)")
name: str = Field(description="Action name within the method (e.g., search, extract)")
register_model_labels(
"ActionSelection",
{"en": "Action Selection", "fr": "Sélection d'action"},
{
"method": {"en": "Method", "fr": "Méthode"},
"name": {"en": "Action Name", "fr": "Nom de l'action"},
},
)
class ActionParameters(BaseModel, ModelMixin):
parameters: Dict[str, Any] = Field(default_factory=dict, description="Parameters to execute the selected action")
register_model_labels(
"ActionParameters",
{"en": "Action Parameters", "fr": "Paramètres d'action"},
{
"parameters": {"en": "Parameters", "fr": "Paramètres"},
},
)
class ObservationPreview(BaseModel, ModelMixin):
name: str = Field(description="Document name or URL label")
mime: str = Field(description="MIME type or kind")
snippet: str = Field(description="Short snippet or summary")
register_model_labels(
"ObservationPreview",
{"en": "Observation Preview", "fr": "Aperçu d'observation"},
{
"name": {"en": "Name", "fr": "Nom"},
"mime": {"en": "MIME", "fr": "MIME"},
"snippet": {"en": "Snippet", "fr": "Extrait"},
},
)
class Observation(BaseModel, ModelMixin):
success: bool = Field(description="Action execution success flag")
resultLabel: str = Field(description="Deterministic label for produced documents")
documentsCount: int = Field(description="Number of produced documents")
previews: List[ObservationPreview] = Field(default_factory=list, description="Compact previews of outputs")
notes: List[str] = Field(default_factory=list, description="Short notes or key facts")
register_model_labels(
"Observation",
{"en": "Observation", "fr": "Observation"},
{
"success": {"en": "Success", "fr": "Succès"},
"resultLabel": {"en": "Result Label", "fr": "Étiquette du résultat"},
"documentsCount": {"en": "Documents Count", "fr": "Nombre de documents"},
"previews": {"en": "Previews", "fr": "Aperçus"},
"notes": {"en": "Notes", "fr": "Notes"},
},
)
class TaskStatus(str):
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
register_model_labels(
"TaskStatus",
{"en": "Task Status", "fr": "Statut de la tâche"},
{
"PENDING": {"en": "Pending", "fr": "En attente"},
"RUNNING": {"en": "Running", "fr": "En cours"},
"COMPLETED": {"en": "Completed", "fr": "Terminé"},
"FAILED": {"en": "Failed", "fr": "Échec"},
"CANCELLED": {"en": "Cancelled", "fr": "Annulé"},
},
)
class DocumentExchange(BaseModel, ModelMixin):
documentsLabel: str = Field(description="Label for the set of documents")
documents: List[str] = Field(default_factory=list, description="List of document references")
register_model_labels(
"DocumentExchange",
{"en": "Document Exchange", "fr": "Échange de documents"},
{
"documentsLabel": {"en": "Documents Label", "fr": "Label des documents"},
"documents": {"en": "Documents", "fr": "Documents"},
},
)
class TaskAction(BaseModel, ModelMixin):
id: str = Field(..., description="Action ID")
execMethod: str = Field(..., description="Method to execute")
execAction: str = Field(..., description="Action to perform")
execParameters: Dict[str, Any] = Field(default_factory=dict, description="Action parameters")
execResultLabel: Optional[str] = Field(None, description="Label for the set of result documents")
expectedDocumentFormats: Optional[List[Dict[str, str]]] = Field(None, description="Expected document formats (optional)")
userMessage: Optional[str] = Field(None, description="User-friendly message in user's language")
status: TaskStatus = Field(default=TaskStatus.PENDING, description="Action status")
error: Optional[str] = Field(None, description="Error message if action failed")
retryCount: int = Field(default=0, description="Number of retries attempted")
retryMax: int = Field(default=3, description="Maximum number of retries")
processingTime: Optional[float] = Field(None, description="Processing time in seconds")
timestamp: float = Field(..., description="When the action was executed (UTC timestamp in seconds)")
result: Optional[str] = Field(None, description="Result of the action")
register_model_labels(
"TaskAction",
{"en": "Task Action", "fr": "Action de tâche"},
{
"id": {"en": "Action ID", "fr": "ID de l'action"},
"execMethod": {"en": "Method", "fr": "Méthode"},
"execAction": {"en": "Action", "fr": "Action"},
"execParameters": {"en": "Parameters", "fr": "Paramètres"},
"execResultLabel": {"en": "Result Label", "fr": "Label du résultat"},
"expectedDocumentFormats": {"en": "Expected Document Formats", "fr": "Formats de documents attendus"},
"userMessage": {"en": "User Message", "fr": "Message utilisateur"},
"status": {"en": "Status", "fr": "Statut"},
"error": {"en": "Error", "fr": "Erreur"},
"retryCount": {"en": "Retry Count", "fr": "Nombre de tentatives"},
"retryMax": {"en": "Max Retries", "fr": "Tentatives max"},
"processingTime": {"en": "Processing Time", "fr": "Temps de traitement"},
"timestamp": {"en": "Timestamp", "fr": "Horodatage"},
"result": {"en": "Result", "fr": "Résultat"},
},
)
class TaskResult(BaseModel, ModelMixin):
taskId: str = Field(..., description="Task ID")
status: TaskStatus = Field(default=TaskStatus.PENDING, description="Task status")
success: bool = Field(..., description="Whether the task was successful")
feedback: Optional[str] = Field(None, description="Task feedback message")
error: Optional[str] = Field(None, description="Error message if task failed")
register_model_labels(
"TaskResult",
{"en": "Task Result", "fr": "Résultat de tâche"},
{
"taskId": {"en": "Task ID", "fr": "ID de la tâche"},
"status": {"en": "Status", "fr": "Statut"},
"success": {"en": "Success", "fr": "Succès"},
"feedback": {"en": "Feedback", "fr": "Retour"},
"error": {"en": "Error", "fr": "Erreur"},
},
)
class TaskItem(BaseModel, ModelMixin):
id: str = Field(..., description="Task ID")
workflowId: str = Field(..., description="Workflow ID")
userInput: str = Field(..., description="User input that triggered the task")
status: TaskStatus = Field(default=TaskStatus.PENDING, description="Task status")
error: Optional[str] = Field(None, description="Error message if task failed")
startedAt: Optional[float] = Field(None, description="When the task started (UTC timestamp in seconds)")
finishedAt: Optional[float] = Field(None, description="When the task finished (UTC timestamp in seconds)")
actionList: List[TaskAction] = Field(default_factory=list, description="List of actions to execute")
retryCount: int = Field(default=0, description="Number of retries attempted")
retryMax: int = Field(default=3, description="Maximum number of retries")
rollbackOnFailure: bool = Field(default=True, description="Whether to rollback on failure")
dependencies: List[str] = Field(default_factory=list, description="List of task IDs this task depends on")
feedback: Optional[str] = Field(None, description="Task feedback message")
processingTime: Optional[float] = Field(None, description="Total processing time in seconds")
resultLabels: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Map of result labels to their values")
register_model_labels(
"TaskItem",
{"en": "Task", "fr": "Tâche"},
{
"id": {"en": "Task ID", "fr": "ID de la tâche"},
"workflowId": {"en": "Workflow ID", "fr": "ID du workflow"},
"userInput": {"en": "User Input", "fr": "Entrée utilisateur"},
"status": {"en": "Status", "fr": "Statut"},
"error": {"en": "Error", "fr": "Erreur"},
"startedAt": {"en": "Started At", "fr": "Démarré à"},
"finishedAt": {"en": "Finished At", "fr": "Terminé à"},
"actionList": {"en": "Actions", "fr": "Actions"},
"retryCount": {"en": "Retry Count", "fr": "Nombre de tentatives"},
"retryMax": {"en": "Max Retries", "fr": "Tentatives max"},
"processingTime": {"en": "Processing Time", "fr": "Temps de traitement"},
},
)
class TaskStep(BaseModel, ModelMixin):
id: str
objective: str
dependencies: Optional[list[str]] = Field(default_factory=list)
success_criteria: Optional[list[str]] = Field(default_factory=list)
estimated_complexity: Optional[str] = None
userMessage: Optional[str] = Field(None, description="User-friendly message in user's language")
register_model_labels(
"TaskStep",
{"en": "Task Step", "fr": "Étape de tâche"},
{
"id": {"en": "ID", "fr": "ID"},
"objective": {"en": "Objective", "fr": "Objectif"},
"dependencies": {"en": "Dependencies", "fr": "Dépendances"},
"success_criteria": {"en": "Success Criteria", "fr": "Critères de succès"},
"estimated_complexity": {"en": "Estimated Complexity", "fr": "Complexité estimée"},
"userMessage": {"en": "User Message", "fr": "Message utilisateur"},
},
)
class TaskHandover(BaseModel, ModelMixin):
taskId: str = Field(description="Target task ID")
sourceTask: Optional[str] = Field(None, description="Source task ID")
inputDocuments: List[DocumentExchange] = Field(default_factory=list, description="Available input documents")
outputDocuments: List[DocumentExchange] = Field(default_factory=list, description="Produced output documents")
context: Dict[str, Any] = Field(default_factory=dict, description="Task context")
previousResults: List[str] = Field(default_factory=list, description="Previous result summaries")
improvements: List[str] = Field(default_factory=list, description="Improvement suggestions")
workflowSummary: Optional[str] = Field(None, description="Summarized workflow context")
messageHistory: List[str] = Field(default_factory=list, description="Key message summaries")
timestamp: float = Field(..., description="When the handover was created (UTC timestamp in seconds)")
handoverType: str = Field(default="task", description="Type of handover: task, phase, or workflow")
register_model_labels(
"TaskHandover",
{"en": "Task Handover", "fr": "Transfert de tâche"},
{
"taskId": {"en": "Task ID", "fr": "ID de la tâche"},
"sourceTask": {"en": "Source Task", "fr": "Tâche source"},
"inputDocuments": {"en": "Input Documents", "fr": "Documents d'entrée"},
"outputDocuments": {"en": "Output Documents", "fr": "Documents de sortie"},
"context": {"en": "Context", "fr": "Contexte"},
"previousResults": {"en": "Previous Results", "fr": "Résultats précédents"},
"improvements": {"en": "Improvements", "fr": "Améliorations"},
"workflowSummary": {"en": "Workflow Summary", "fr": "Résumé du workflow"},
"messageHistory": {"en": "Message History", "fr": "Historique des messages"},
"timestamp": {"en": "Timestamp", "fr": "Horodatage"},
"handoverType": {"en": "Handover Type", "fr": "Type de transfert"},
},
)
class TaskContext(BaseModel, ModelMixin):
task_step: TaskStep
workflow: Optional['ChatWorkflow'] = None
workflow_id: Optional[str] = None
available_documents: Optional[str] = "No documents available"
available_connections: Optional[list[str]] = Field(default_factory=list)
previous_results: Optional[list[str]] = Field(default_factory=list)
previous_handover: Optional[TaskHandover] = None
improvements: Optional[list[str]] = Field(default_factory=list)
retry_count: Optional[int] = 0
previous_action_results: Optional[list] = Field(default_factory=list)
previous_review_result: Optional[dict] = None
is_regeneration: Optional[bool] = False
failure_patterns: Optional[list[str]] = Field(default_factory=list)
failed_actions: Optional[list] = Field(default_factory=list)
successful_actions: Optional[list] = Field(default_factory=list)
criteria_progress: Optional[dict] = None
def getDocumentReferences(self) -> List[str]:
docs = []
if self.previous_handover:
for doc_exchange in self.previous_handover.inputDocuments:
docs.extend(doc_exchange.documents)
return list(set(docs))
def addImprovement(self, improvement: str) -> None:
if improvement not in (self.improvements or []):
if self.improvements is None:
self.improvements = []
self.improvements.append(improvement)
class ReviewContext(BaseModel, ModelMixin):
task_step: TaskStep
task_actions: Optional[list] = Field(default_factory=list)
action_results: Optional[list] = Field(default_factory=list)
step_result: Optional[dict] = Field(default_factory=dict)
workflow_id: Optional[str] = None
previous_results: Optional[list[str]] = Field(default_factory=list)
class ReviewResult(BaseModel, ModelMixin):
status: str
reason: Optional[str] = None
improvements: Optional[list[str]] = Field(default_factory=list)
quality_score: Optional[int] = 5
missing_outputs: Optional[list[str]] = Field(default_factory=list)
met_criteria: Optional[list[str]] = Field(default_factory=list)
unmet_criteria: Optional[list[str]] = Field(default_factory=list)
confidence: Optional[float] = 0.5
userMessage: Optional[str] = Field(None, description="User-friendly message in user's language")
register_model_labels(
"ReviewResult",
{"en": "Review Result", "fr": "Résultat de l'évaluation"},
{
"status": {"en": "Status", "fr": "Statut"},
"reason": {"en": "Reason", "fr": "Raison"},
"improvements": {"en": "Improvements", "fr": "Améliorations"},
"quality_score": {"en": "Quality Score", "fr": "Score de qualité"},
"missing_outputs": {"en": "Missing Outputs", "fr": "Sorties manquantes"},
"met_criteria": {"en": "Met Criteria", "fr": "Critères respectés"},
"unmet_criteria": {"en": "Unmet Criteria", "fr": "Critères non respectés"},
"confidence": {"en": "Confidence", "fr": "Confiance"},
"userMessage": {"en": "User Message", "fr": "Message utilisateur"},
},
)
class TaskPlan(BaseModel, ModelMixin):
overview: str
tasks: list[TaskStep]
userMessage: Optional[str] = Field(None, description="Overall user-friendly message for the task plan")
register_model_labels(
"TaskPlan",
{"en": "Task Plan", "fr": "Plan de tâches"},
{
"overview": {"en": "Overview", "fr": "Aperçu"},
"tasks": {"en": "Tasks", "fr": "Tâches"},
"userMessage": {"en": "User Message", "fr": "Message utilisateur"},
},
)
class WorkflowResult(BaseModel, ModelMixin):
status: str
completed_tasks: int
total_tasks: int
execution_time: float
final_results_count: int
error: Optional[str] = None
phase: Optional[str] = None
register_model_labels(
"WorkflowResult",
{"en": "Workflow Result", "fr": "Résultat du workflow"},
{
"status": {"en": "Status", "fr": "Statut"},
"completed_tasks": {"en": "Completed Tasks", "fr": "Tâches terminées"},
"total_tasks": {"en": "Total Tasks", "fr": "Total des tâches"},
"execution_time": {"en": "Execution Time", "fr": "Temps d'exécution"},
"final_results_count": {"en": "Final Results Count", "fr": "Nombre de résultats finaux"},
"error": {"en": "Error", "fr": "Erreur"},
"phase": {"en": "Phase", "fr": "Phase"},
},
)
class UserInputRequest(BaseModel, ModelMixin):
prompt: str = Field(description="Prompt for the user")
listFileId: List[str] = Field(default_factory=list, description="List of file IDs")
userLanguage: str = Field(default="en", description="User's preferred language")
register_model_labels(
"UserInputRequest",
{"en": "User Input Request", "fr": "Demande de saisie utilisateur"},
{
"prompt": {"en": "Prompt", "fr": "Invite"},
"listFileId": {"en": "File IDs", "fr": "IDs des fichiers"},
"userLanguage": {"en": "User Language", "fr": "Langue de l'utilisateur"},
},
)

View file

@ -2,8 +2,9 @@ import logging
import asyncio import asyncio
from typing import Optional from typing import Optional
from modules.interfaces.interfaceAppModel import User from modules.datamodels.datamodelUam import User
from modules.interfaces.interfaceChatModel import ChatWorkflow, UserInputRequest from modules.datamodels.datamodelChat import ChatWorkflow
from modules.datamodels.datamodelWorkflow import UserInputRequest
from modules.shared.timezoneUtils import get_utc_timestamp from modules.shared.timezoneUtils import get_utc_timestamp
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -1,7 +1,8 @@
import logging import logging
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from modules.interfaces.interfaceAppModel import User, DataNeutralizerAttributes, DataNeutraliserConfig from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelNeutralizer import DataNeutralizerAttributes, DataNeutraliserConfig
from modules.services.serviceNeutralization.mainServiceNeutralization import NeutralizationService from modules.services.serviceNeutralization.mainServiceNeutralization import NeutralizationService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -161,7 +162,7 @@ class SharepointProcessor:
async def _getSharepointConnection(self, sharepointPath: str = None): async def _getSharepointConnection(self, sharepointPath: str = None):
try: try:
from modules.interfaces.interfaceAppModel import UserConnection from modules.datamodels.datamodelUam import UserConnection
connections = self.service.app_interface.db.getRecordset( connections = self.service.app_interface.db.getRecordset(
UserConnection, UserConnection,
recordFilter={"userId": self.service.app_interface.userId} recordFilter={"userId": self.service.app_interface.userId}

View file

@ -850,13 +850,13 @@ try:
misfire_grace_time=1800, misfire_grace_time=1800,
) )
logger.info("Registered DG ticket sync via EventManagement (every 20 minutes)") logger.info("Registered DG ticket sync via EventManagement (every 20 minutes)")
# Run initial sync
#import asyncio
#asyncio.create_task(scheduled_sync())
#logger.info("Initial sync scheduled")
else: else:
logger.info(f"Skipping DG scheduler registration for ticket sync in env: {APP_ENV_TYPE}") logger.info(f"Skipping DG scheduler registration for ticket sync in env: {APP_ENV_TYPE}")
# Run initial sync
#import asyncio
#asyncio.create_task(scheduled_sync())
#logger.info("Initial sync scheduled")
except Exception as e: except Exception as e:
logger.error(f"Failed to register DG ticket sync: {str(e)}") logger.error(f"Failed to register DG ticket sync: {str(e)}")

View file

@ -3,7 +3,7 @@ from typing import Dict, Any, List
from modules.connectors.connectorAiOpenai import AiOpenai from modules.connectors.connectorAiOpenai import AiOpenai
from modules.connectors.connectorAiAnthropic import AiAnthropic from modules.connectors.connectorAiAnthropic import AiAnthropic
from modules.interfaces.interfaceAiModel import AiCallOptions, AiCallRequest, AiCallResponse from modules.datamodels.datamodelAi import AiCallOptions, AiCallRequest, AiCallResponse
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -5,7 +5,8 @@ Access control for the Application.
import logging import logging
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from datetime import datetime from datetime import datetime
from modules.interfaces.interfaceAppModel import UserPrivilege, User, UserInDB, AuthEvent, Mandate from modules.datamodels.datamodelUam import UserPrivilege, User, UserInDB, Mandate
from modules.datamodels.datamodelSecurity import AuthEvent
from modules.shared.timezoneUtils import get_utc_now from modules.shared.timezoneUtils import get_utc_now
# Configure logger # Configure logger

View file

@ -1,566 +0,0 @@
"""
Models for User Service
"""
import uuid
from pydantic import BaseModel, Field, EmailStr
from typing import List, Dict, Any, Optional
from datetime import datetime
from enum import Enum
from modules.shared.attributeUtils import register_model_labels, AttributeDefinition, ModelMixin
from modules.shared.timezoneUtils import get_utc_timestamp
class AuthAuthority(str, Enum):
"""Authentication authority enum"""
LOCAL = "local"
GOOGLE = "google"
MSFT = "msft"
class UserPrivilege(str, Enum):
"""User privilege levels"""
SYSADMIN = "sysadmin"
ADMIN = "admin"
USER = "user"
class ConnectionStatus(str, Enum):
"""Connection status"""
ACTIVE = "active"
EXPIRED = "expired"
REVOKED = "revoked"
PENDING = "pending"
class TokenStatus(str, Enum):
"""Status of an issued gateway JWT access token"""
ACTIVE = "active"
REVOKED = "revoked"
class Mandate(BaseModel, ModelMixin):
"""Data model for a mandate"""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the mandate",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
name: str = Field(
description="Name of the mandate",
frontend_type="text",
frontend_readonly=False,
frontend_required=True
)
language: str = Field(
default="en",
description="Default language of the mandate",
frontend_type="select",
frontend_readonly=False,
frontend_required=True,
frontend_options=[
{"value": "de", "label": {"en": "Deutsch", "fr": "Allemand"}},
{"value": "en", "label": {"en": "English", "fr": "Anglais"}},
{"value": "fr", "label": {"en": "Français", "fr": "Français"}},
{"value": "it", "label": {"en": "Italiano", "fr": "Italien"}}
]
)
enabled: bool = Field(
default=True,
description="Indicates whether the mandate is enabled",
frontend_type="checkbox",
frontend_readonly=False,
frontend_required=False
)
# Register labels for Mandate
register_model_labels(
"Mandate",
{"en": "Mandate", "fr": "Mandat"},
{
"id": {"en": "ID", "fr": "ID"},
"name": {"en": "Name", "fr": "Nom"},
"language": {"en": "Language", "fr": "Langue"},
"enabled": {"en": "Enabled", "fr": "Activé"}
}
)
class UserConnection(BaseModel, ModelMixin):
"""Data model for a user's connection to an external service"""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the connection",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
userId: str = Field(
description="ID of the user this connection belongs to",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
authority: AuthAuthority = Field(
description="Authentication authority",
frontend_type="select",
frontend_readonly=True,
frontend_required=False,
frontend_options=[
{"value": "local", "label": {"en": "Local", "fr": "Local"}},
{"value": "google", "label": {"en": "Google", "fr": "Google"}},
{"value": "msft", "label": {"en": "Microsoft", "fr": "Microsoft"}}
]
)
externalId: str = Field(
description="User ID in the external system",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
externalUsername: str = Field(
description="Username in the external system",
frontend_type="text",
frontend_readonly=False,
frontend_required=False
)
externalEmail: Optional[EmailStr] = Field(
None,
description="Email in the external system",
frontend_type="email",
frontend_readonly=False,
frontend_required=False
)
status: ConnectionStatus = Field(
default=ConnectionStatus.ACTIVE,
description="Connection status",
frontend_type="select",
frontend_readonly=False,
frontend_required=False,
frontend_options=[
{"value": "active", "label": {"en": "Active", "fr": "Actif"}},
{"value": "inactive", "label": {"en": "Inactive", "fr": "Inactif"}},
{"value": "expired", "label": {"en": "Expired", "fr": "Expiré"}},
{"value": "pending", "label": {"en": "Pending", "fr": "En attente"}}
]
)
connectedAt: float = Field(
default_factory=get_utc_timestamp,
description="When the connection was established (UTC timestamp in seconds)",
frontend_type="timestamp",
frontend_readonly=True,
frontend_required=False
)
lastChecked: float = Field(
default_factory=get_utc_timestamp,
description="When the connection was last verified (UTC timestamp in seconds)",
frontend_type="timestamp",
frontend_readonly=True,
frontend_required=False
)
expiresAt: Optional[float] = Field(
None,
description="When the connection expires (UTC timestamp in seconds)",
frontend_type="timestamp",
frontend_readonly=True,
frontend_required=False
)
tokenStatus: Optional[str] = Field(
None,
description="Current token status: active, expired, none",
frontend_type="select",
frontend_readonly=True,
frontend_required=False,
frontend_options=[
{"value": "active", "label": {"en": "Active", "fr": "Actif"}},
{"value": "expired", "label": {"en": "Expired", "fr": "Expiré"}},
{"value": "none", "label": {"en": "None", "fr": "Aucun"}}
]
)
tokenExpiresAt: Optional[float] = Field(
None,
description="When the current token expires (UTC timestamp in seconds)",
frontend_type="timestamp",
frontend_readonly=True,
frontend_required=False
)
# Register labels for UserConnection
register_model_labels(
"UserConnection",
{"en": "User Connection", "fr": "Connexion utilisateur"},
{
"id": {"en": "ID", "fr": "ID"},
"userId": {"en": "User ID", "fr": "ID utilisateur"},
"authority": {"en": "Authority", "fr": "Autorité"},
"externalId": {"en": "External ID", "fr": "ID externe"},
"externalUsername": {"en": "External Username", "fr": "Nom d'utilisateur externe"},
"externalEmail": {"en": "External Email", "fr": "Email externe"},
"status": {"en": "Status", "fr": "Statut"},
"connectedAt": {"en": "Connected At", "fr": "Connecté le"},
"lastChecked": {"en": "Last Checked", "fr": "Dernière vérification"},
"expiresAt": {"en": "Expires At", "fr": "Expire le"},
"tokenStatus": {"en": "Connection Status", "fr": "Statut de connexion"},
"tokenExpiresAt": {"en": "Expires At", "fr": "Expire le"}
}
)
class User(BaseModel, ModelMixin):
"""Data model for a user"""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the user",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
username: str = Field(
description="Username for login",
frontend_type="text",
frontend_readonly=False,
frontend_required=True
)
email: Optional[EmailStr] = Field(
None,
description="Email address of the user",
frontend_type="email",
frontend_readonly=False,
frontend_required=True
)
fullName: Optional[str] = Field(
None,
description="Full name of the user",
frontend_type="text",
frontend_readonly=False,
frontend_required=False
)
language: str = Field(
default="en",
description="Preferred language of the user",
frontend_type="select",
frontend_readonly=False,
frontend_required=True,
frontend_options=[
{"value": "de", "label": {"en": "Deutsch", "fr": "Allemand"}},
{"value": "en", "label": {"en": "English", "fr": "Anglais"}},
{"value": "fr", "label": {"en": "Français", "fr": "Français"}},
{"value": "it", "label": {"en": "Italiano", "fr": "Italien"}}
]
)
enabled: bool = Field(
default=True,
description="Indicates whether the user is enabled",
frontend_type="checkbox",
frontend_readonly=False,
frontend_required=False
)
privilege: UserPrivilege = Field(
default=UserPrivilege.USER,
description="Permission level",
frontend_type="select",
frontend_readonly=False,
frontend_required=True,
frontend_options=[
{"value": "user", "label": {"en": "User", "fr": "Utilisateur"}},
{"value": "admin", "label": {"en": "Admin", "fr": "Administrateur"}},
{"value": "sysadmin", "label": {"en": "SysAdmin", "fr": "Administrateur système"}}
]
)
authenticationAuthority: AuthAuthority = Field(
default=AuthAuthority.LOCAL,
description="Primary authentication authority",
frontend_type="select",
frontend_readonly=True,
frontend_required=False,
frontend_options=[
{"value": "local", "label": {"en": "Local", "fr": "Local"}},
{"value": "google", "label": {"en": "Google", "fr": "Google"}},
{"value": "msft", "label": {"en": "Microsoft", "fr": "Microsoft"}}
]
)
mandateId: Optional[str] = Field(
None,
description="ID of the mandate this user belongs to",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
# Register labels for User
register_model_labels(
"User",
{"en": "User", "fr": "Utilisateur"},
{
"id": {"en": "ID", "fr": "ID"},
"username": {"en": "Username", "fr": "Nom d'utilisateur"},
"email": {"en": "Email", "fr": "Email"},
"fullName": {"en": "Full Name", "fr": "Nom complet"},
"language": {"en": "Language", "fr": "Langue"},
"enabled": {"en": "Enabled", "fr": "Activé"},
"privilege": {"en": "Privilege", "fr": "Privilège"},
"authenticationAuthority": {"en": "Auth Authority", "fr": "Autorité d'authentification"},
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"}
}
)
class UserInDB(User):
"""Extended user class with password hash"""
hashedPassword: Optional[str] = Field(None, description="Hash of the user password")
# Register labels for UserInDB
register_model_labels(
"UserInDB",
{"en": "User Access", "fr": "Accès de l'utilisateur"},
{
"hashedPassword": {"en": "Password hash", "fr": "Hachage de mot de passe"}
}
)
# Token Models
class Token(BaseModel, ModelMixin):
"""Token model for all authentication types"""
id: Optional[str] = None
userId: str
authority: AuthAuthority
connectionId: Optional[str] = Field(None, description="ID of the connection this token belongs to")
tokenAccess: str
tokenType: str = "bearer"
expiresAt: float = Field(description="When the token expires (UTC timestamp in seconds)")
tokenRefresh: Optional[str] = None
createdAt: Optional[float] = Field(None, description="When the token was created (UTC timestamp in seconds)")
# Revocation and session tracking (for LOCAL gateway JWTs)
status: TokenStatus = Field(default=TokenStatus.ACTIVE, description="Token status: active/revoked")
revokedAt: Optional[float] = Field(None, description="When the token was revoked (UTC timestamp in seconds)")
revokedBy: Optional[str] = Field(None, description="User ID who revoked the token (admin/self)")
reason: Optional[str] = Field(None, description="Optional revocation reason")
sessionId: Optional[str] = Field(None, description="Logical session grouping for logout revocation")
mandateId: Optional[str] = Field(None, description="Mandate ID for tenant scoping of the token")
class Config:
useEnumValues = True
# Register labels for Token
register_model_labels(
"Token",
{"en": "Token", "fr": "Jeton"},
{
"id": {"en": "ID", "fr": "ID"},
"userId": {"en": "User ID", "fr": "ID utilisateur"},
"authority": {"en": "Authority", "fr": "Autorité"},
"connectionId": {"en": "Connection ID", "fr": "ID de connexion"},
"tokenAccess": {"en": "Access Token", "fr": "Jeton d'accès"},
"tokenType": {"en": "Token Type", "fr": "Type de jeton"},
"expiresAt": {"en": "Expires At", "fr": "Expire le"},
"tokenRefresh": {"en": "Refresh Token", "fr": "Jeton de rafraîchissement"},
"createdAt": {"en": "Created At", "fr": "Créé le"},
"status": {"en": "Status", "fr": "Statut"},
"revokedAt": {"en": "Revoked At", "fr": "Révoqué le"},
"revokedBy": {"en": "Revoked By", "fr": "Révoqué par"},
"reason": {"en": "Reason", "fr": "Raison"},
"sessionId": {"en": "Session ID", "fr": "ID de session"},
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"}
}
)
class LocalToken(Token):
"""Local authentication token model"""
pass
class GoogleToken(Token):
"""Google OAuth token model"""
pass
class MsftToken(Token):
"""Microsoft OAuth token model"""
pass
class AuthEvent(BaseModel, ModelMixin):
"""Data model for authentication events"""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the auth event",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
userId: str = Field(
description="ID of the user this event belongs to",
frontend_type="text",
frontend_readonly=True,
frontend_required=True
)
eventType: str = Field(
description="Type of authentication event (e.g., 'login', 'logout', 'token_refresh')",
frontend_type="text",
frontend_readonly=True,
frontend_required=True
)
timestamp: float = Field(
default_factory=get_utc_timestamp,
description="Unix timestamp when the event occurred",
frontend_type="datetime",
frontend_readonly=True,
frontend_required=True
)
ipAddress: Optional[str] = Field(
default=None,
description="IP address from which the event originated",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
userAgent: Optional[str] = Field(
default=None,
description="User agent string from the request",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
success: bool = Field(
default=True,
description="Whether the authentication event was successful",
frontend_type="boolean",
frontend_readonly=True,
frontend_required=True
)
details: Optional[str] = Field(
default=None,
description="Additional details about the event",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
# Register labels for AuthEvent
register_model_labels(
"AuthEvent",
{"en": "Authentication Event", "fr": "Événement d'authentification"},
{
"id": {"en": "ID", "fr": "ID"},
"userId": {"en": "User ID", "fr": "ID utilisateur"},
"eventType": {"en": "Event Type", "fr": "Type d'événement"},
"timestamp": {"en": "Timestamp", "fr": "Horodatage"},
"ipAddress": {"en": "IP Address", "fr": "Adresse IP"},
"userAgent": {"en": "User Agent", "fr": "Agent utilisateur"},
"success": {"en": "Success", "fr": "Succès"},
"details": {"en": "Details", "fr": "Détails"}
}
)
class DataNeutraliserConfig(BaseModel, ModelMixin):
"""Data model for data neutralization configuration"""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the configuration",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
mandateId: str = Field(
description="ID of the mandate this configuration belongs to",
frontend_type="text",
frontend_readonly=True,
frontend_required=True
)
userId: str = Field(
description="ID of the user who created this configuration",
frontend_type="text",
frontend_readonly=True,
frontend_required=True
)
enabled: bool = Field(
default=True,
description="Whether data neutralization is enabled",
frontend_type="checkbox",
frontend_readonly=False,
frontend_required=False
)
namesToParse: str = Field(
default="",
description="Multiline list of names to parse for neutralization",
frontend_type="textarea",
frontend_readonly=False,
frontend_required=False
)
sharepointSourcePath: str = Field(
default="",
description="SharePoint path to read files for neutralization",
frontend_type="text",
frontend_readonly=False,
frontend_required=False
)
sharepointTargetPath: str = Field(
default="",
description="SharePoint path to store neutralized files",
frontend_type="text",
frontend_readonly=False,
frontend_required=False
)
# Register labels for DataNeutraliserConfig
register_model_labels(
"DataNeutraliserConfig",
{"en": "Data Neutralization Config", "fr": "Configuration de neutralisation des données"},
{
"id": {"en": "ID", "fr": "ID"},
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
"userId": {"en": "User ID", "fr": "ID utilisateur"},
"enabled": {"en": "Enabled", "fr": "Activé"},
"namesToParse": {"en": "Names to Parse", "fr": "Noms à analyser"},
"sharepointSourcePath": {"en": "Source Path", "fr": "Chemin source"},
"sharepointTargetPath": {"en": "Target Path", "fr": "Chemin cible"}
}
)
class DataNeutralizerAttributes(BaseModel, ModelMixin):
"""Data model for neutralized data attributes mapping"""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the attribute mapping (used as UID in neutralized files)",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
mandateId: str = Field(
description="ID of the mandate this attribute belongs to",
frontend_type="text",
frontend_readonly=True,
frontend_required=True
)
userId: str = Field(
description="ID of the user who created this attribute",
frontend_type="text",
frontend_readonly=True,
frontend_required=True
)
originalText: str = Field(
description="Original text that was neutralized",
frontend_type="text",
frontend_readonly=True,
frontend_required=True
)
fileId: Optional[str] = Field(
default=None,
description="ID of the file this attribute belongs to",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
patternType: str = Field(
description="Type of pattern that matched (email, phone, name, etc.)",
frontend_type="text",
frontend_readonly=True,
frontend_required=True
)
# Register labels for DataNeutralizerAttributes
register_model_labels(
"DataNeutralizerAttributes",
{"en": "Neutralized Data Attribute", "fr": "Attribut de données neutralisées"},
{
"id": {"en": "ID", "fr": "ID"},
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
"userId": {"en": "User ID", "fr": "ID utilisateur"},
"originalText": {"en": "Original Text", "fr": "Texte original"},
"fileId": {"en": "File ID", "fr": "ID de fichier"},
"patternType": {"en": "Pattern Type", "fr": "Type de modèle"}
}
)

View file

@ -17,12 +17,12 @@ from modules.connectors.connectorDbPostgre import DatabaseConnector
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.shared.timezoneUtils import get_utc_now, get_utc_timestamp from modules.shared.timezoneUtils import get_utc_now, get_utc_timestamp
from modules.interfaces.interfaceAppAccess import AppAccess from modules.interfaces.interfaceAppAccess import AppAccess
from modules.interfaces.interfaceAppModel import ( from modules.datamodels.datamodelUam import (
User, Mandate, UserInDB, UserConnection, User, Mandate, UserInDB, UserConnection,
AuthAuthority, UserPrivilege, AuthAuthority, UserPrivilege, ConnectionStatus,
ConnectionStatus, Token, AuthEvent, TokenStatus,
DataNeutraliserConfig, DataNeutralizerAttributes
) )
from modules.datamodels.datamodelSecurity import Token, AuthEvent, TokenStatus
from modules.datamodels.datamodelNeutralizer import DataNeutraliserConfig, DataNeutralizerAttributes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -4,8 +4,8 @@ Handles user access management and permission checks.
""" """
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from modules.interfaces.interfaceAppModel import User, UserPrivilege from modules.datamodels.datamodelUam import User, UserPrivilege
from modules.interfaces.interfaceChatModel import ChatWorkflow, ChatMessage, ChatLog, ChatStat, ChatDocument from modules.datamodels.datamodelChat import ChatWorkflow, ChatMessage, ChatLog, ChatStat, ChatDocument
class ChatAccess: class ChatAccess:
""" """

View file

@ -12,10 +12,10 @@ from typing import Dict, Any, List, Optional, Union, get_origin, get_args
import asyncio import asyncio
from modules.interfaces.interfaceChatAccess import ChatAccess from modules.interfaces.interfaceChatAccess import ChatAccess
from modules.interfaces.interfaceChatModel import ( from modules.datamodels.datamodelWorkflow import UserInputRequest, TaskAction, TaskResult
TaskStatus, UserInputRequest, ChatDocument, TaskItem, ChatStat, ChatLog, ChatMessage, ChatWorkflow, TaskAction, TaskResult, ActionResult from modules.datamodels.datamodelWorkflow import TaskItem, TaskStatus, ActionResult
) from modules.datamodels.datamodelChat import ChatDocument, ChatStat, ChatLog, ChatMessage, ChatWorkflow
from modules.interfaces.interfaceAppModel import User from modules.datamodels.datamodelUam import User
# DYNAMIC PART: Connectors to the Interface # DYNAMIC PART: Connectors to the Interface
from modules.connectors.connectorDbPostgre import DatabaseConnector from modules.connectors.connectorDbPostgre import DatabaseConnector

View file

@ -5,9 +5,11 @@ Handles user access management and permission checks.
import logging import logging
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from modules.interfaces.interfaceAppModel import User, UserInDB from modules.datamodels.datamodelUam import User, UserInDB
from modules.interfaces.interfaceComponentModel import Prompt, FileItem, FileData, VoiceSettings from modules.datamodels.datamodelUtils import Prompt
from modules.interfaces.interfaceChatModel import ChatWorkflow, ChatMessage, ChatLog from modules.datamodels.datamodelFiles import FileItem, FileData
from modules.datamodels.datamodelVoice import VoiceSettings
from modules.datamodels.datamodelChat import ChatWorkflow, ChatMessage, ChatLog
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -1,264 +0,0 @@
"""
Service Management model classes for the service management system.
Updated to match the Entity Relation Diagram structure.
"""
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional, Union
from datetime import datetime
import uuid
# Import for label registration
from modules.shared.attributeUtils import register_model_labels, ModelMixin
from modules.shared.timezoneUtils import get_utc_timestamp
# CORE MODELS
class FileItem(BaseModel, ModelMixin):
"""Data model for a file item"""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
mandateId: str = Field(
description="ID of the mandate this file belongs to",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
fileName: str = Field(
description="Name of the file",
frontend_type="text",
frontend_readonly=False,
frontend_required=True
)
mimeType: str = Field(
description="MIME type of the file",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
fileHash: str = Field(
description="Hash of the file",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
fileSize: int = Field(
description="Size of the file in bytes",
frontend_type="integer",
frontend_readonly=True,
frontend_required=False
)
creationDate: float = Field(
default_factory=get_utc_timestamp,
description="Date when the file was created (UTC timestamp in seconds)",
frontend_type="timestamp",
frontend_readonly=True,
frontend_required=False
)
def to_dict(self) -> Dict[str, Any]:
"""Convert model to dictionary"""
return super().to_dict()
# Register labels for FileItem
register_model_labels(
"FileItem",
{"en": "File Item", "fr": "Élément de fichier"},
{
"id": {"en": "ID", "fr": "ID"},
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
"fileName": {"en": "fileName", "fr": "Nom de fichier"},
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
"fileHash": {"en": "File Hash", "fr": "Hash du fichier"},
"fileSize": {"en": "File Size", "fr": "Taille du fichier"},
"creationDate": {"en": "Creation Date", "fr": "Date de création"}
}
)
class FilePreview(BaseModel, ModelMixin):
"""Data model for file preview"""
content: Union[str, bytes] = Field(description="File content (text or binary)")
mimeType: str = Field(description="MIME type of the file")
fileName: str = Field(description="Original fileName")
isText: bool = Field(description="Whether the content is text (True) or binary (False)")
encoding: Optional[str] = Field(None, description="Text encoding if content is text")
size: int = Field(description="Size of the content in bytes")
def to_dict(self) -> Dict[str, Any]:
"""Convert model to dictionary with proper content handling"""
data = super().to_dict()
# Convert bytes to base64 string if content is binary
if isinstance(data.get("content"), bytes):
import base64
data["content"] = base64.b64encode(data["content"]).decode('utf-8')
return data
# Register labels for FilePreview
register_model_labels(
"FilePreview",
{"en": "File Preview", "fr": "Aperçu du fichier"},
{
"content": {"en": "Content", "fr": "Contenu"},
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
"fileName": {"en": "fileName", "fr": "Nom de fichier"},
"isText": {"en": "Is Text", "fr": "Est du texte"},
"encoding": {"en": "Encoding", "fr": "Encodage"},
"size": {"en": "Size", "fr": "Taille"}
}
)
class FileData(BaseModel, ModelMixin):
"""Data model for file data"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
data: str = Field(description="File data content")
base64Encoded: bool = Field(description="Whether the data is base64 encoded")
# Register labels for FileData
register_model_labels(
"FileData",
{"en": "File Data", "fr": "Données de fichier"},
{
"id": {"en": "ID", "fr": "ID"},
"data": {"en": "Data", "fr": "Données"},
"base64Encoded": {"en": "Base64 Encoded", "fr": "Encodé en Base64"}
}
)
class Prompt(BaseModel, ModelMixin):
"""Data model for a prompt"""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
mandateId: str = Field(
description="ID of the mandate this prompt belongs to",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
content: str = Field(
description="Content of the prompt",
frontend_type="textarea",
frontend_readonly=False,
frontend_required=True
)
name: str = Field(
description="Name of the prompt",
frontend_type="text",
frontend_readonly=False,
frontend_required=True
)
# Register labels for Prompt
register_model_labels(
"Prompt",
{"en": "Prompt", "fr": "Invite"},
{
"id": {"en": "ID", "fr": "ID"},
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
"content": {"en": "Content", "fr": "Contenu"},
"name": {"en": "Name", "fr": "Nom"}
}
)
class VoiceSettings(BaseModel, ModelMixin):
"""Data model for voice service settings per user"""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
userId: str = Field(
description="ID of the user these settings belong to",
frontend_type="text",
frontend_readonly=True,
frontend_required=True
)
mandateId: str = Field(
description="ID of the mandate these settings belong to",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
sttLanguage: str = Field(
default="de-DE",
description="Speech-to-Text language",
frontend_type="select",
frontend_readonly=False,
frontend_required=True
)
ttsLanguage: str = Field(
default="de-DE",
description="Text-to-Speech language",
frontend_type="select",
frontend_readonly=False,
frontend_required=True
)
ttsVoice: str = Field(
default="de-DE-KatjaNeural",
description="Text-to-Speech voice",
frontend_type="select",
frontend_readonly=False,
frontend_required=True
)
translationEnabled: bool = Field(
default=True,
description="Whether translation is enabled",
frontend_type="checkbox",
frontend_readonly=False,
frontend_required=False
)
targetLanguage: str = Field(
default="en-US",
description="Target language for translation",
frontend_type="select",
frontend_readonly=False,
frontend_required=False
)
creationDate: float = Field(
default_factory=get_utc_timestamp,
description="Date when the settings were created (UTC timestamp in seconds)",
frontend_type="timestamp",
frontend_readonly=True,
frontend_required=False
)
lastModified: float = Field(
default_factory=get_utc_timestamp,
description="Date when the settings were last modified (UTC timestamp in seconds)",
frontend_type="timestamp",
frontend_readonly=True,
frontend_required=False
)
def to_dict(self) -> Dict[str, Any]:
"""Convert model to dictionary"""
return super().to_dict()
# Register labels for VoiceSettings
register_model_labels(
"VoiceSettings",
{"en": "Voice Settings", "fr": "Paramètres vocaux"},
{
"id": {"en": "ID", "fr": "ID"},
"userId": {"en": "User ID", "fr": "ID utilisateur"},
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
"sttLanguage": {"en": "STT Language", "fr": "Langue STT"},
"ttsLanguage": {"en": "TTS Language", "fr": "Langue TTS"},
"ttsVoice": {"en": "TTS Voice", "fr": "Voix TTS"},
"translationEnabled": {"en": "Translation Enabled", "fr": "Traduction activée"},
"targetLanguage": {"en": "Target Language", "fr": "Langue cible"},
"creationDate": {"en": "Creation Date", "fr": "Date de création"},
"lastModified": {"en": "Last Modified", "fr": "Dernière modification"}
}
)

View file

@ -11,10 +11,10 @@ from typing import Dict, Any, List, Optional, Union
import hashlib import hashlib
from modules.interfaces.interfaceComponentAccess import ComponentAccess from modules.interfaces.interfaceComponentAccess import ComponentAccess
from modules.interfaces.interfaceComponentModel import ( from modules.datamodels.datamodelFiles import FilePreview, FileItem, FileData
FilePreview, Prompt, FileItem, FileData, VoiceSettings from modules.datamodels.datamodelUtils import Prompt
) from modules.datamodels.datamodelVoice import VoiceSettings
from modules.interfaces.interfaceAppModel import User, Mandate from modules.datamodels.datamodelUam import User, Mandate
# DYNAMIC PART: Connectors to the Interface # DYNAMIC PART: Connectors to the Interface
from modules.connectors.connectorDbPostgre import DatabaseConnector from modules.connectors.connectorDbPostgre import DatabaseConnector

View file

@ -1,8 +1,6 @@
from typing import Any, Optional from typing import Any, Optional
from datetime import datetime, timezone from datetime import datetime, timezone
from modules.interfaces.interfaceTicketModel import TicketBase, Task
# Module-level factory to create TicketInterface by connector type # Module-level factory to create TicketInterface by connector type
async def createTicketInterfaceByType( async def createTicketInterfaceByType(
*, *,
@ -28,18 +26,22 @@ async def createTicketInterfaceByType(
class TicketInterface: class TicketInterface:
def __init__(self, *, connector_ticket: TicketBase, task_sync_definition: dict): def __init__(self, *, connector_ticket, task_sync_definition: dict):
self.connector_ticket = connector_ticket self.connector_ticket = connector_ticket
self.task_sync_definition = task_sync_definition self.task_sync_definition = task_sync_definition
async def exportTicketsAsList(self) -> list[dict]: async def exportTicketsAsList(self) -> list[dict]:
tickets = await self.connector_ticket.read_tasks(limit=0) tickets: list[dict] = await self.connector_ticket.read_tasks(limit=0)
transformed_tasks = self._transformTasks(tickets, includePut=True) transformed_tasks = self._transformTicketRecords(tickets, includePut=True)
data_list = [task.data for task in transformed_tasks] # Return plain dictionaries filtered by presence of ID
return self._filterEmptyRecords(data_list) rows: list[dict] = []
for task in transformed_tasks:
if isinstance(task, dict) and task.get("ID"):
rows.append(task)
return rows
async def importListToTickets(self, records: list[dict]) -> None: async def importListToTickets(self, records: list[dict]) -> None:
updates: list[Task] = [] updates: list[dict] = []
for row in records: for row in records:
task_id = row.get("ID") task_id = row.get("ID")
if not task_id: if not task_id:
@ -53,15 +55,15 @@ class TicketInterface:
field_id = field_path[1] field_id = field_path[1]
fields[field_id] = value fields[field_id] = value
if fields: if fields:
updates.append(Task(data={"ID": task_id, "fields": fields})) updates.append({"ID": task_id, "fields": fields})
if updates: if updates:
await self.connector_ticket.write_tasks(updates) await self.connector_ticket.write_tasks(updates)
def _transformTasks( def _transformTicketRecords(
self, tasks: list[Task], includePut: bool = False self, tasks: list[dict], includePut: bool = False
) -> list[Task]: ) -> list[dict]:
"""Transforms tasks according to the task_sync_definition.""" """Transforms tasks according to the task_sync_definition."""
transformed_tasks = [] transformed_tasks: list[dict] = []
for task in tasks: for task in tasks:
transformed_data = {} transformed_data = {}
@ -73,12 +75,10 @@ class TicketInterface:
# Get the right fields # Get the right fields
if direction == "get" or includePut: if direction == "get" or includePut:
value = self._extractFieldValue(task.data, field_path, field_name) value = self._extractFieldValue(task, field_path, field_name)
transformed_data[field_name] = value transformed_data[field_name] = value
# Create new Task with transformed data transformed_tasks.append(transformed_data)
transformed_task = Task(data=transformed_data)
transformed_tasks.append(transformed_task)
return transformed_tasks return transformed_tasks
@ -173,14 +173,5 @@ class TicketInterface:
return any(keyword in field_name.lower() for keyword in date_keywords) return any(keyword in field_name.lower() for keyword in date_keywords)
def _filterEmptyRecords(self, records: list[dict]) -> list[dict]: def _filterEmptyRecords(self, records: list[dict]) -> list[dict]:
"""Remove records that are missing an ID. return [row for row in records if isinstance(row, dict) and row.get("ID")]
Purposefully only filter by presence of 'ID' to avoid dropping
valid rows with many empty optional fields.
"""
filtered: list[dict] = []
for row in records:
if isinstance(row, dict) and row.get("ID"):
filtered.append(row)
return filtered

View file

@ -1,113 +1,51 @@
from typing import Optional
import json
import csv
import io
from modules.interfaces.interfaceWebModel import (
WebCrawlActionResult,
WebSearchActionResult,
WebSearchRequest,
WebCrawlRequest,
WebScrapeActionResult,
WebScrapeRequest,
WebCrawlDocumentData,
WebScrapeDocumentData,
WebSearchDocumentData,
)
from dataclasses import dataclass from dataclasses import dataclass
from modules.connectors.connectorWebTavily import ConnectorTavily
from modules.interfaces.interfaceChatModel import ActionDocument
from modules.datamodels.datamodelWeb import (
WebCrawlActionResult,
WebCrawlActionDocument,
WebCrawlDocumentData,
WebCrawlRequest,
WebCrawlResultItem,
WebScrapeActionResult,
WebScrapeActionDocument,
WebSearchDocumentData as WebScrapeDocumentData,
WebScrapeRequest,
WebScrapeResultItem,
WebSearchActionResult,
WebSearchActionDocument,
WebSearchDocumentData,
WebSearchRequest,
WebSearchResultItem,
)
from modules.connectors.connectorWebTavily import ConnectorWeb
from modules.datamodels.datamodelWorkflow import ActionDocument
@dataclass(slots=True) @dataclass(slots=True)
class WebInterface: class WebInterface:
connectorWebTavily: ConnectorTavily connectorWebTavily: ConnectorWeb
def __post_init__(self) -> None: def __post_init__(self) -> None:
if self.connectorWebTavily is None: if self.connectorWebTavily is None:
raise TypeError( raise TypeError(
"connectorWebTavily must be provided. " "connectorWebTavily must be provided. "
"Use `await WebInterface.create()` or pass a ConnectorTavily." "Use `await WebInterface.create()` or pass a ConnectorWeb."
) )
@classmethod @classmethod
async def create(cls) -> "WebInterface": async def create(cls) -> "WebInterface":
connectorWebTavily = await ConnectorTavily.create() connectorWebTavily = await ConnectorWeb.create()
return WebInterface(connectorWebTavily=connectorWebTavily) return WebInterface(connectorWebTavily=connectorWebTavily)
# Methods
async def search(self, web_search_request: WebSearchRequest) -> WebSearchActionResult: async def search(self, web_search_request: WebSearchRequest) -> WebSearchActionResult:
# NOTE: Add connectors here return await self.connectorWebTavily.search(web_search_request)
return await self.connectorWebTavily.search_urls(web_search_request)
async def crawl(self, web_crawl_request: WebCrawlRequest) -> WebCrawlActionResult: async def crawl(self, web_crawl_request: WebCrawlRequest) -> WebCrawlActionResult:
# NOTE: Add connectors here return await self.connectorWebTavily.crawl(web_crawl_request)
return await self.connectorWebTavily.crawl_urls(web_crawl_request)
async def scrape(self, web_scrape_request: WebScrapeRequest) -> WebScrapeActionResult: async def scrape(self, web_scrape_request: WebScrapeRequest) -> WebScrapeActionResult:
# NOTE: Add connectors here
return await self.connectorWebTavily.scrape(web_scrape_request) return await self.connectorWebTavily.scrape(web_scrape_request)
def convert_web_result_to_json(self, web_result) -> str: # Helpers moved to MethodWeb
"""Convert WebCrawlActionResult or WebScrapeActionResult to proper JSON format"""
if not web_result.success or not web_result.documents:
return json.dumps({"success": web_result.success, "error": web_result.error})
# Extract the document data and convert to dict
document_data = web_result.documents[0].documentData
# Convert Pydantic model to dict
result_dict = {
"success": web_result.success,
"results": [
{
"url": str(result.url),
"content": result.content
}
for result in document_data.results
],
"total_count": document_data.total_count
}
# Add type-specific fields
if hasattr(document_data, 'urls'):
# WebCrawlDocumentData has urls field
result_dict["urls"] = [str(url) for url in document_data.urls]
elif hasattr(document_data, 'query'):
# WebScrapeDocumentData has query field
result_dict["query"] = document_data.query
return json.dumps(result_dict, indent=2, ensure_ascii=False)
def convert_web_search_result_to_csv(self, web_search_result: WebSearchActionResult) -> str:
"""Convert WebSearchActionResult to CSV format with url and title columns"""
if not web_search_result.success or not web_search_result.documents:
return ""
output = io.StringIO()
writer = csv.writer(output, delimiter=';')
# Write header
writer.writerow(['url', 'title'])
# Write data rows
document_data = web_search_result.documents[0].documentData
for result in document_data.results:
writer.writerow([str(result.url), result.title])
return output.getvalue()
def create_json_action_document(self, json_content: str, document_name: str) -> ActionDocument:
"""Create an ActionDocument with JSON content"""
return ActionDocument(
documentName=document_name,
documentData=json_content,
mimeType="application/json"
)
def create_csv_action_document(self, csv_content: str, document_name: str) -> ActionDocument:
"""Create an ActionDocument with CSV content"""
return ActionDocument(
documentName=document_name,
documentData=csv_content,
mimeType="text/csv"
)

View file

@ -10,7 +10,7 @@ from datetime import datetime
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.security.auth import limiter, getCurrentUser from modules.security.auth import limiter, getCurrentUser
from modules.interfaces.interfaceAppModel import User from modules.datamodels.datamodelUam import User
from modules.interfaces.interfaceAppObjects import getRootInterface from modules.interfaces.interfaceAppObjects import getRootInterface
# Static folder setup - using absolute path from app root # Static folder setup - using absolute path from app root

View file

@ -11,7 +11,7 @@ import logging
from modules.security.auth import limiter, getCurrentUser from modules.security.auth import limiter, getCurrentUser
# Import the attribute definition and helper functions # Import the attribute definition and helper functions
from modules.interfaces.interfaceAppModel import User from modules.datamodels.datamodelUam import User
from modules.shared.attributeUtils import getModelClasses, getModelAttributeDefinitions, AttributeResponse, AttributeDefinition from modules.shared.attributeUtils import getModelClasses, getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
# Configure logger # Configure logger

View file

@ -16,11 +16,9 @@ import modules.interfaces.interfaceChatObjects as interfaceChatObjects
from modules.interfaces.interfaceChatObjects import getInterface from modules.interfaces.interfaceChatObjects import getInterface
# Import models # Import models
from modules.interfaces.interfaceChatModel import ( from modules.datamodels.datamodelChat import ChatWorkflow
ChatWorkflow, from modules.datamodels.datamodelWorkflow import UserInputRequest
UserInputRequest from modules.datamodels.datamodelUam import User
)
from modules.interfaces.interfaceAppModel import User
# Import workflow control functions # Import workflow control functions
from modules.features.chatPlayground.mainChatPlayground import chatStart, chatStop from modules.features.chatPlayground.mainChatPlayground import chatStart, chatStop

View file

@ -15,7 +15,8 @@ from datetime import datetime
import logging import logging
import json import json
from modules.interfaces.interfaceAppModel import User, UserConnection, AuthAuthority, ConnectionStatus, Token from modules.datamodels.datamodelUam import User, UserConnection, AuthAuthority, ConnectionStatus
from modules.datamodels.datamodelSecurity import Token
from modules.security.auth import getCurrentUser, limiter from modules.security.auth import getCurrentUser, limiter
from modules.interfaces.interfaceAppObjects import getInterface, getRootInterface from modules.interfaces.interfaceAppObjects import getInterface, getRootInterface
from modules.shared.timezoneUtils import get_utc_timestamp from modules.shared.timezoneUtils import get_utc_timestamp

View file

@ -15,9 +15,9 @@ from modules.security.auth import limiter, getCurrentUser
# Import interfaces # Import interfaces
import modules.interfaces.interfaceComponentObjects as interfaceComponentObjects import modules.interfaces.interfaceComponentObjects as interfaceComponentObjects
from modules.interfaces.interfaceComponentModel import FileItem, FilePreview from modules.datamodels.datamodelFiles import FileItem, FilePreview
from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
from modules.interfaces.interfaceAppModel import User from modules.datamodels.datamodelUam import User
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -21,7 +21,7 @@ import modules.interfaces.interfaceAppObjects as interfaceAppObjects
from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
# Import the model classes # Import the model classes
from modules.interfaces.interfaceAppModel import Mandate, User from modules.datamodels.datamodelUam import Mandate, User
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -6,7 +6,8 @@ import logging
from modules.security.auth import limiter, getCurrentUser from modules.security.auth import limiter, getCurrentUser
# Import interfaces # Import interfaces
from modules.interfaces.interfaceAppModel import User, DataNeutraliserConfig, DataNeutralizerAttributes from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelNeutralizer import DataNeutraliserConfig, DataNeutralizerAttributes
from modules.features.neutralizePlayground.mainNeutralizePlayground import NeutralizationPlayground from modules.features.neutralizePlayground.mainNeutralizePlayground import NeutralizationPlayground
# Configure logger # Configure logger

View file

@ -13,9 +13,9 @@ from modules.security.auth import limiter, getCurrentUser
# Import interfaces # Import interfaces
import modules.interfaces.interfaceComponentObjects as interfaceComponentObjects import modules.interfaces.interfaceComponentObjects as interfaceComponentObjects
from modules.interfaces.interfaceComponentModel import Prompt from modules.datamodels.datamodelUtils import Prompt
from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
from modules.interfaces.interfaceAppModel import User from modules.datamodels.datamodelUam import User
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -18,7 +18,8 @@ import modules.interfaces.interfaceAppObjects as interfaceAppObjects
from modules.security.auth import getCurrentUser, limiter, getCurrentUser from modules.security.auth import getCurrentUser, limiter, getCurrentUser
# Import the attribute definition and helper functions # Import the attribute definition and helper functions
from modules.interfaces.interfaceAppModel import User, AttributeDefinition from modules.datamodels.datamodelUam import User
from modules.shared.attributeUtils import AttributeDefinition
from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse
# Configure logger # Configure logger
@ -177,7 +178,7 @@ async def reset_user_password(
# SECURITY: Automatically revoke all tokens for the user after password reset # SECURITY: Automatically revoke all tokens for the user after password reset
try: try:
from modules.interfaces.interfaceAppModel import AuthAuthority from modules.datamodels.datamodelUam import AuthAuthority
revoked_count = appInterface.revokeTokensByUser( revoked_count = appInterface.revokeTokensByUser(
userId=userId, userId=userId,
authority=None, # Revoke all authorities authority=None, # Revoke all authorities
@ -253,7 +254,7 @@ async def change_password(
# SECURITY: Automatically revoke all tokens for the user after password change # SECURITY: Automatically revoke all tokens for the user after password change
try: try:
from modules.interfaces.interfaceAppModel import AuthAuthority from modules.datamodels.datamodelUam import AuthAuthority
revoked_count = appInterface.revokeTokensByUser( revoked_count = appInterface.revokeTokensByUser(
userId=str(currentUser.id), userId=str(currentUser.id),
authority=None, # Revoke all authorities authority=None, # Revoke all authorities

View file

@ -6,7 +6,8 @@ import logging
from modules.security.auth import getCurrentUser, limiter from modules.security.auth import getCurrentUser, limiter
from modules.interfaces.interfaceAppObjects import getInterface, getRootInterface from modules.interfaces.interfaceAppObjects import getInterface, getRootInterface
from modules.interfaces.interfaceAppModel import User, UserInDB, AuthAuthority, Token from modules.datamodels.datamodelUam import User, UserInDB, AuthAuthority
from modules.datamodels.datamodelSecurity import Token
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -13,7 +13,8 @@ import httpx
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.interfaces.interfaceAppObjects import getInterface, getRootInterface from modules.interfaces.interfaceAppObjects import getInterface, getRootInterface
from modules.interfaces.interfaceAppModel import AuthAuthority, User, Token, ConnectionStatus, UserConnection from modules.datamodels.datamodelUam import AuthAuthority, User, ConnectionStatus, UserConnection
from modules.datamodels.datamodelSecurity import Token
from modules.security.auth import getCurrentUser, limiter from modules.security.auth import getCurrentUser, limiter
from modules.shared.attributeUtils import ModelMixin from modules.shared.attributeUtils import ModelMixin
from modules.shared.timezoneUtils import get_utc_now, create_expiration_timestamp, get_utc_timestamp from modules.shared.timezoneUtils import get_utc_now, create_expiration_timestamp, get_utc_timestamp
@ -169,7 +170,7 @@ async def login(
try: try:
if connectionId: if connectionId:
rootInterface = getRootInterface() rootInterface = getRootInterface()
from modules.interfaces.interfaceAppModel import UserConnection from modules.datamodels.datamodelUam import UserConnection
records = rootInterface.db.getRecordset(UserConnection, recordFilter={"id": connectionId}) records = rootInterface.db.getRecordset(UserConnection, recordFilter={"id": connectionId})
if records: if records:
record = records[0] record = records[0]
@ -208,7 +209,7 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
"""Handle Google OAuth callback""" """Handle Google OAuth callback"""
try: try:
# Import Token at function level to avoid scoping issues # Import Token at function level to avoid scoping issues
from modules.interfaces.interfaceAppModel import Token from modules.datamodels.datamodelSecurity import Token
# Parse state # Parse state
state_data = json.loads(state) state_data = json.loads(state)
@ -469,7 +470,7 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
connection.externalEmail = user_info.get("email") connection.externalEmail = user_info.get("email")
# Update connection record directly # Update connection record directly
from modules.interfaces.interfaceAppModel import UserConnection from modules.datamodels.datamodelUam import UserConnection
rootInterface.db.recordModify(UserConnection, connection_id, connection.to_dict()) rootInterface.db.recordModify(UserConnection, connection_id, connection.to_dict())

View file

@ -16,7 +16,8 @@ from pydantic import BaseModel
from modules.security.auth import getCurrentUser, limiter, SECRET_KEY, ALGORITHM from modules.security.auth import getCurrentUser, limiter, SECRET_KEY, ALGORITHM
from modules.security.jwtService import createAccessToken, createRefreshToken, setAccessTokenCookie, setRefreshTokenCookie from modules.security.jwtService import createAccessToken, createRefreshToken, setAccessTokenCookie, setRefreshTokenCookie
from modules.interfaces.interfaceAppObjects import getInterface, getRootInterface from modules.interfaces.interfaceAppObjects import getInterface, getRootInterface
from modules.interfaces.interfaceAppModel import User, UserInDB, AuthAuthority, UserPrivilege, Token from modules.datamodels.datamodelUam import User, UserInDB, AuthAuthority, UserPrivilege
from modules.datamodels.datamodelSecurity import Token
from modules.shared.attributeUtils import ModelMixin from modules.shared.attributeUtils import ModelMixin
# Configure logger # Configure logger
@ -56,7 +57,7 @@ async def login(
rootInterface = getRootInterface() rootInterface = getRootInterface()
# Get default mandate ID # Get default mandate ID
from modules.interfaces.interfaceAppModel import Mandate from modules.datamodels.datamodelUam import Mandate
defaultMandateId = rootInterface.getInitialId(Mandate) defaultMandateId = rootInterface.getInitialId(Mandate)
if not defaultMandateId: if not defaultMandateId:
raise HTTPException( raise HTTPException(
@ -197,7 +198,7 @@ async def register_user(
appInterface = getRootInterface() appInterface = getRootInterface()
# Get default mandate ID # Get default mandate ID
from modules.interfaces.interfaceAppModel import Mandate from modules.datamodels.datamodelUam import Mandate
defaultMandateId = appInterface.getInitialId(Mandate) defaultMandateId = appInterface.getInitialId(Mandate)
if not defaultMandateId: if not defaultMandateId:
raise HTTPException( raise HTTPException(
@ -210,7 +211,7 @@ async def register_user(
# Create user with local authentication # Create user with local authentication
# Set safe default privilege level for new registrations # Set safe default privilege level for new registrations
from modules.interfaces.interfaceAppModel import UserPrivilege from modules.datamodels.datamodelUam import UserPrivilege
user = appInterface.createUser( user = appInterface.createUser(
username=userData.username, username=userData.username,
password=password, password=password,

View file

@ -13,7 +13,8 @@ import httpx
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.interfaces.interfaceAppObjects import getInterface, getRootInterface from modules.interfaces.interfaceAppObjects import getInterface, getRootInterface
from modules.interfaces.interfaceAppModel import AuthAuthority, User, Token, ConnectionStatus, UserConnection from modules.datamodels.datamodelUam import AuthAuthority, User, ConnectionStatus, UserConnection
from modules.datamodels.datamodelSecurity import Token
from modules.security.auth import getCurrentUser, limiter from modules.security.auth import getCurrentUser, limiter
from modules.security.jwtService import createAccessToken from modules.security.jwtService import createAccessToken
from modules.shared.attributeUtils import ModelMixin from modules.shared.attributeUtils import ModelMixin

View file

@ -10,7 +10,7 @@ from fastapi.responses import Response
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
from modules.connectors.connectorGoogleSpeech import ConnectorGoogleSpeech from modules.connectors.connectorGoogleSpeech import ConnectorGoogleSpeech
from modules.security.auth import getCurrentUser from modules.security.auth import getCurrentUser
from modules.interfaces.interfaceAppModel import User from modules.datamodels.datamodelUam import User
from modules.interfaces.interfaceComponentObjects import getInterface from modules.interfaces.interfaceComponentObjects import getInterface
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -19,15 +19,15 @@ import modules.interfaces.interfaceChatObjects as interfaceChatObjects
from modules.interfaces.interfaceChatObjects import getInterface from modules.interfaces.interfaceChatObjects import getInterface
# Import models # Import models
from modules.interfaces.interfaceChatModel import ( from modules.datamodels.datamodelChat import (
ChatWorkflow, ChatWorkflow,
ChatMessage, ChatMessage,
ChatLog, ChatLog,
ChatStat, ChatStat,
ChatDocument ChatDocument,
) )
from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse
from modules.interfaces.interfaceAppModel import User from modules.datamodels.datamodelUam import User
from modules.shared.timezoneUtils import get_utc_timestamp from modules.shared.timezoneUtils import get_utc_timestamp

View file

@ -16,7 +16,8 @@ from slowapi.util import get_remote_address
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.shared.timezoneUtils import get_utc_now, get_utc_timestamp from modules.shared.timezoneUtils import get_utc_now, get_utc_timestamp
from modules.interfaces.interfaceAppObjects import getRootInterface from modules.interfaces.interfaceAppObjects import getRootInterface
from modules.interfaces.interfaceAppModel import User, AuthAuthority, Token from modules.datamodels.datamodelUam import User, AuthAuthority
from modules.datamodels.datamodelSecurity import Token
# Get Config Data # Get Config Data
SECRET_KEY = APP_CONFIG.get("APP_JWT_KEY_SECRET") SECRET_KEY = APP_CONFIG.get("APP_JWT_KEY_SECRET")

View file

@ -8,7 +8,8 @@ import httpx
from datetime import datetime from datetime import datetime
from typing import Optional, Dict, Any, Callable from typing import Optional, Dict, Any, Callable
from modules.interfaces.interfaceAppModel import Token, AuthAuthority from modules.datamodels.datamodelSecurity import Token
from modules.datamodels.datamodelUam import AuthAuthority
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.shared.timezoneUtils import get_utc_timestamp, create_expiration_timestamp from modules.shared.timezoneUtils import get_utc_timestamp, create_expiration_timestamp

View file

@ -10,7 +10,8 @@ import logging
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from datetime import datetime, timedelta from datetime import datetime, timedelta
from modules.interfaces.interfaceAppObjects import getInterface from modules.interfaces.interfaceAppObjects import getInterface
from modules.interfaces.interfaceAppModel import User, UserConnection, AuthAuthority, Token from modules.datamodels.datamodelUam import User, UserConnection, AuthAuthority
from modules.datamodels.datamodelSecurity import Token
from modules.shared.timezoneUtils import get_utc_timestamp from modules.shared.timezoneUtils import get_utc_timestamp
from modules.shared.auditLogger import audit_logger from modules.shared.auditLogger import audit_logger

View file

@ -1,7 +1,7 @@
from typing import Any from typing import Any
from modules.interfaces.interfaceAppModel import User from modules.datamodels.datamodelUam import User
from modules.interfaces.interfaceChatModel import ChatWorkflow from modules.datamodels.datamodelChat import ChatWorkflow
class PublicService: class PublicService:
"""Lightweight proxy exposing only public callable attributes of a target. """Lightweight proxy exposing only public callable attributes of a target.
@ -75,6 +75,9 @@ class Services:
from .serviceWorkflow.mainServiceWorkflow import WorkflowService from .serviceWorkflow.mainServiceWorkflow import WorkflowService
self.workflow = PublicService(WorkflowService(self)) self.workflow = PublicService(WorkflowService(self))
from .serviceWeb.mainServiceWeb import WebService
self.web = PublicService(WebService(self))
def getInterface(user: User, workflow: ChatWorkflow) -> Services: def getInterface(user: User, workflow: ChatWorkflow) -> Services:

View file

@ -1,9 +1,9 @@
import logging import logging
from typing import Dict, Any, List, Optional, Tuple from typing import Dict, Any, List, Optional, Tuple
from modules.interfaces.interfaceChatModel import ChatDocument from modules.datamodels.datamodelChat import ChatDocument
from modules.services.serviceDocument.mainServiceDocumentExtraction import DocumentExtractionService from modules.services.serviceDocument.mainServiceDocumentExtraction import DocumentExtractionService
from modules.interfaces.interfaceAiModel import AiCallRequest, AiCallOptions from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions
from modules.interfaces.interfaceAiObjects import AiObjects from modules.interfaces.interfaceAiObjects import AiObjects
@ -15,8 +15,6 @@ logger = logging.getLogger(__name__)
class AiService: class AiService:
"""Centralized AI service orchestrating documents, model selection and failover. """Centralized AI service orchestrating documents, model selection and failover.
The concrete connector instances (OpenAI/Anthropic) are injected by the interface layer.
""" """
def __init__(self, serviceCenter=None) -> None: def __init__(self, serviceCenter=None) -> None:
@ -60,10 +58,6 @@ class AiService:
logger.error(f"Error in centralized AI call: {str(e)}") logger.error(f"Error in centralized AI call: {str(e)}")
return f"Error: {str(e)}" return f"Error: {str(e)}"
# Model selection now handled by interface AiObjects
# Cost estimation handled by interface for model selection
async def _processDocumentsForAi( async def _processDocumentsForAi(
self, self,
documents: List[ChatDocument], documents: List[ChatDocument],
@ -113,8 +107,6 @@ class AiService:
return "\n\n---\n\n".join(processedContents) return "\n\n---\n\n".join(processedContents)
# Prompt/context optimization (compression) handled by interface
async def _compressContent(self, content: str, targetSize: int, contentType: str) -> str: async def _compressContent(self, content: str, targetSize: int, contentType: str) -> str:
if len(content.encode("utf-8")) <= targetSize: if len(content.encode("utf-8")) <= targetSize:
return content return content
@ -136,8 +128,3 @@ class AiService:
logger.warning(f"AI compression failed, using truncation: {str(e)}") logger.warning(f"AI compression failed, using truncation: {str(e)}")
return content[:targetSize] + "... [truncated]" return content[:targetSize] + "... [truncated]"
# Failover logic now centralized in interface via model selection; service delegates a single call
# Fallback selection moved to interface; service doesn't select models directly

View file

@ -17,11 +17,8 @@ from modules.services.serviceDocument.documentUtility import (
convertDocumentDataToString convertDocumentDataToString
) )
from modules.interfaces.interfaceChatModel import ( from modules.datamodels.datamodelWorkflow import ExtractedContent
ExtractedContent, from modules.datamodels.datamodelChat import ContentItem, ContentMetadata
ContentItem,
ContentMetadata
)
from modules.services.serviceNeutralization.mainServiceNeutralization import NeutralizationService from modules.services.serviceNeutralization.mainServiceNeutralization import NeutralizationService
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
@ -1424,7 +1421,7 @@ class DocumentExtractionService:
Original prompt: {prompt} Original prompt: {prompt}
""" """
from modules.interfaces.interfaceChatModel import ChatDocument from modules.datamodels.datamodelChat import ChatDocument
image_doc = ChatDocument(fileData=chunk, fileName="image", mimeType=mimeType) image_doc = ChatDocument(fileData=chunk, fileName="image", mimeType=mimeType)
# Use direct import to avoid circular dependency # Use direct import to avoid circular dependency
from modules.services.serviceAi.mainServiceAi import AiService from modules.services.serviceAi.mainServiceAi import AiService

View file

@ -11,7 +11,7 @@ import re
import json import json
from typing import Dict, List, Any, Optional from typing import Dict, List, Any, Optional
from modules.interfaces.interfaceAppModel import DataNeutraliserConfig, DataNeutralizerAttributes from modules.datamodels.datamodelNeutralizer import DataNeutraliserConfig, DataNeutralizerAttributes
# Import all necessary classes and functions for neutralization # Import all necessary classes and functions for neutralization
from modules.services.serviceNeutralization.subProcessCommon import CommonUtils, NeutralizationResult, NeutralizationAttribute from modules.services.serviceNeutralization.subProcessCommon import CommonUtils, NeutralizationResult, NeutralizationAttribute

View file

@ -0,0 +1,49 @@
import logging
from typing import Optional, List
from modules.datamodels.datamodelWeb import (
WebSearchRequest,
WebCrawlRequest,
WebScrapeRequest,
WebSearchActionResult,
WebCrawlActionResult,
WebScrapeActionResult,
)
from modules.interfaces.interfaceWebObjects import WebInterface
logger = logging.getLogger(__name__)
class WebService:
"""Centralized Web service providing wrappers around web interface actions.
"""
def __init__(self, serviceCenter=None) -> None:
self.serviceCenter = serviceCenter
async def webSearch(self, request: WebSearchRequest) -> WebSearchActionResult:
try:
web_interface = await WebInterface.create()
return await web_interface.search(request)
except Exception as e:
logger.error(f"Error in webSearch: {str(e)}")
raise
async def webCrawl(self, request: WebCrawlRequest) -> WebCrawlActionResult:
try:
web_interface = await WebInterface.create()
return await web_interface.crawl(request)
except Exception as e:
logger.error(f"Error in webCrawl: {str(e)}")
raise
async def webScrape(self, request: WebScrapeRequest) -> WebScrapeActionResult:
try:
web_interface = await WebInterface.create()
return await web_interface.scrape(request)
except Exception as e:
logger.error(f"Error in webScrape: {str(e)}")
raise

View file

@ -1,8 +1,9 @@
import logging import logging
import uuid import uuid
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from modules.interfaces.interfaceAppModel import User, UserConnection from modules.datamodels.datamodelUam import User, UserConnection
from modules.interfaces.interfaceChatModel import ChatDocument, ChatMessage, ExtractedContent from modules.datamodels.datamodelChat import ChatDocument, ChatMessage
from modules.datamodels.datamodelChat import ExtractedContent
from modules.services.serviceDocument.mainServiceDocumentExtraction import DocumentExtractionService from modules.services.serviceDocument.mainServiceDocumentExtraction import DocumentExtractionService
from modules.services.serviceDocument.documentUtility import getFileExtension, getMimeTypeFromExtension, detectContentTypeFromData from modules.services.serviceDocument.documentUtility import getFileExtension, getMimeTypeFromExtension, detectContentTypeFromData
from modules.shared.timezoneUtils import get_utc_timestamp from modules.shared.timezoneUtils import get_utc_timestamp

View file

@ -8,7 +8,7 @@ from typing import Dict, Any, List, Optional
from datetime import datetime, UTC from datetime import datetime, UTC
from modules.workflows.methods.methodBase import MethodBase, action from modules.workflows.methods.methodBase import MethodBase, action
from modules.interfaces.interfaceChatModel import ActionResult from modules.datamodels.datamodelWorkflow import ActionResult
from modules.shared.timezoneUtils import get_utc_timestamp from modules.shared.timezoneUtils import get_utc_timestamp
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -9,7 +9,7 @@ from typing import Dict, Any, List, Optional
from datetime import datetime, UTC from datetime import datetime, UTC
from modules.workflows.methods.methodBase import MethodBase, action from modules.workflows.methods.methodBase import MethodBase, action
from modules.interfaces.interfaceChatModel import ActionResult from modules.datamodels.datamodelWorkflow import ActionResult, ChatDocument
from modules.shared.timezoneUtils import get_utc_timestamp from modules.shared.timezoneUtils import get_utc_timestamp
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -768,12 +768,11 @@ SOURCE DOCUMENT CONTENT:
# Build ChatDocument list from chatDocuments # Build ChatDocument list from chatDocuments
documents = [] documents = []
try: try:
from modules.interfaces.interfaceChatModel import ChatDocument as ChatDoc
for d in validDocuments: for d in validDocuments:
try: try:
data = self.service.getFileData(d.fileId) if hasattr(d, 'fileId') else None data = self.service.getFileData(d.fileId) if hasattr(d, 'fileId') else None
if data: if data:
documents.append(ChatDoc(fileData=data, fileName=d.fileName, mimeType=d.mimeType)) documents.append(ChatDocument(fileData=data, fileName=d.fileName, mimeType=d.mimeType))
except Exception: except Exception:
continue continue
except Exception: except Exception:

View file

@ -1,90 +1,22 @@
""" """
Microsoft Outlook Email Operations Module Microsoft Outlook Email Operations Module
This module provides actions for composing and sending emails via Microsoft Outlook using the Microsoft Graph API.
ACTION CONTRACT DEFINITION:
==========================
1. COMPOSE EMAIL ACTION (composeEmail):
====================================
Purpose: Use AI to compose professional email content
Input Parameters:
- context (str): Email context/requirements
- recipient (str, optional): Recipient information
- attachments (List[str], optional): Available documents to reference
- tone (str, optional): Email tone (formal, casual, etc.)
- expectedDocumentFormats (list, optional): Ignored - always produces JSON
Output Contract:
The action produces a JSON document with this EXACT structure:
{
"context": "original context",
"recipient": "recipient info",
"tone": "email tone",
"timestamp": "ISO timestamp",
"usage": "usage description",
"to": ["email@example.com"],
"subject": "Email subject",
"body": "Email body content",
"cc": [],
"bcc": [],
"attachments": ["docItem:uuid:fileName.pdf"]
}
Key Points:
- Email fields (to, subject, body, cc, bcc, attachments) are at ROOT LEVEL
- NOT wrapped in a "composedEmail" field
- Always produces .json format regardless of expectedDocumentFormats
- AI response is validated and parsed before output
2. SEND EMAIL ACTION (sendEmail):
==============================
Purpose: Send the composed email via Outlook (creates draft)
Input Parameters:
- connectionReference (str): Microsoft connection reference
- composedEmail (str): Reference to composed email document (docItem:...)
- expectedDocumentFormats (list, optional): Expected output formats
Input Contract:
The composedEmail document MUST have this EXACT structure:
{
"to": ["email@example.com"],
"subject": "Email subject",
"body": "Email body content",
"cc": [],
"bcc": [],
"attachments": ["docItem:uuid:fileName.pdf"]
}
Key Points:
- Email fields must be at ROOT LEVEL
- NOT wrapped in a nested structure
- Reads file content from database using fileId
- Creates email draft in Outlook Drafts folder
- Returns success/failure status
DATA FLOW:
==========
composeEmail JSON Document sendEmail Outlook Draft
The contract ensures that composeEmail outputs exactly what sendEmail expects to consume.
""" """
import base64
import re
import logging import logging
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from datetime import datetime, UTC from datetime import datetime, UTC
import json import json
import uuid import uuid
import requests
from modules.workflows.methods.methodBase import MethodBase, action from modules.workflows.methods.methodBase import MethodBase, action
from modules.interfaces.interfaceChatModel import ActionResult from modules.datamodels.datamodelWorkflow import ActionResult, ChatDocument
from modules.interfaces.interfaceAppModel import ConnectionStatus from modules.datamodels.datamodelUam import ConnectionStatus
from modules.shared.timezoneUtils import get_utc_timestamp from modules.shared.timezoneUtils import get_utc_timestamp
from modules.services import getInterface as getServices
from modules.security.tokenManager import TokenManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -97,7 +29,6 @@ class MethodOutlook(MethodBase):
self.name = "outlook" self.name = "outlook"
self.description = "Handle Microsoft Outlook email operations" self.description = "Handle Microsoft Outlook email operations"
# Centralized services interface (for AI) # Centralized services interface (for AI)
from modules.services import getInterface as getServices
self.services = getServices(self.service.user, self.service.workflow) self.services = getServices(self.service.user, self.service.workflow)
def _format_timestamp_for_filename(self) -> str: def _format_timestamp_for_filename(self) -> str:
@ -120,7 +51,6 @@ class MethodOutlook(MethodBase):
logger.debug(f"Found connection: {userConnection.id}, status: {userConnection.status.value}, authority: {userConnection.authority.value}") logger.debug(f"Found connection: {userConnection.id}, status: {userConnection.status.value}, authority: {userConnection.authority.value}")
# Get a fresh token for this specific connection # Get a fresh token for this specific connection
from modules.security.tokenManager import TokenManager
token = TokenManager().getFreshToken(self.service.interfaceApp, userConnection.id) token = TokenManager().getFreshToken(self.service.interfaceApp, userConnection.id)
if not token: if not token:
logger.error(f"Token not found for connection: {userConnection.id}") logger.error(f"Token not found for connection: {userConnection.id}")
@ -156,8 +86,6 @@ class MethodOutlook(MethodBase):
Check if the current connection has the necessary permissions for Outlook operations. Check if the current connection has the necessary permissions for Outlook operations.
""" """
try: try:
import requests
graph_url = "https://graph.microsoft.com/v1.0" graph_url = "https://graph.microsoft.com/v1.0"
headers = { headers = {
"Authorization": f"Bearer {connection['accessToken']}", "Authorization": f"Bearer {connection['accessToken']}",
@ -215,7 +143,6 @@ class MethodOutlook(MethodBase):
# For basic text search, ensure it's safe for contains() filter # For basic text search, ensure it's safe for contains() filter
# Remove any characters that might break the OData filter syntax # Remove any characters that might break the OData filter syntax
import re
# Remove or escape characters that could break OData filter syntax # Remove or escape characters that could break OData filter syntax
safe_query = re.sub(r'[\\\'"]', '', clean_query) safe_query = re.sub(r'[\\\'"]', '', clean_query)
@ -322,8 +249,6 @@ class MethodOutlook(MethodBase):
This is needed for proper filtering when using advanced search queries This is needed for proper filtering when using advanced search queries
""" """
try: try:
import requests
graph_url = "https://graph.microsoft.com/v1.0" graph_url = "https://graph.microsoft.com/v1.0"
headers = { headers = {
"Authorization": f"Bearer {connection['accessToken']}", "Authorization": f"Bearer {connection['accessToken']}",
@ -412,8 +337,6 @@ class MethodOutlook(MethodBase):
# Read emails using Microsoft Graph API # Read emails using Microsoft Graph API
try: try:
import requests
# Microsoft Graph API endpoint for messages # Microsoft Graph API endpoint for messages
graph_url = "https://graph.microsoft.com/v1.0" graph_url = "https://graph.microsoft.com/v1.0"
headers = { headers = {
@ -619,7 +542,6 @@ class MethodOutlook(MethodBase):
# Parse the email data (should be JSON) # Parse the email data (should be JSON)
if isinstance(email_data, str): if isinstance(email_data, str):
import json
try: try:
# First try to parse as direct JSON # First try to parse as direct JSON
parsed_email_data = json.loads(email_data) parsed_email_data = json.loads(email_data)
@ -629,10 +551,6 @@ class MethodOutlook(MethodBase):
logger.error(f"JSON parsing error: {str(e)}") logger.error(f"JSON parsing error: {str(e)}")
logger.error(f"Content that failed to parse: {repr(email_data[:500])}") logger.error(f"Content that failed to parse: {repr(email_data[:500])}")
# If that fails, try to extract JSON from HTML content
import re
# Look for JSON content within HTML tags or as a script # Look for JSON content within HTML tags or as a script
json_pattern = r'\{[^{}]*"to"[^{}]*"subject"[^{}]*"body"[^{}]*\}' json_pattern = r'\{[^{}]*"to"[^{}]*"subject"[^{}]*"body"[^{}]*\}'
json_match = re.search(json_pattern, email_data, re.DOTALL) json_match = re.search(json_pattern, email_data, re.DOTALL)
@ -686,8 +604,6 @@ class MethodOutlook(MethodBase):
# Create email draft using Microsoft Graph API # Create email draft using Microsoft Graph API
try: try:
import requests
# Microsoft Graph API endpoint for creating draft messages # Microsoft Graph API endpoint for creating draft messages
graph_url = "https://graph.microsoft.com/v1.0" graph_url = "https://graph.microsoft.com/v1.0"
headers = { headers = {
@ -740,7 +656,6 @@ class MethodOutlook(MethodBase):
file_content = self.service.getFileData(file_id) file_content = self.service.getFileData(file_id)
if file_content: if file_content:
# Convert to base64 for Graph API # Convert to base64 for Graph API
import base64
if isinstance(file_content, bytes): if isinstance(file_content, bytes):
content_bytes = file_content content_bytes = file_content
else: else:
@ -903,8 +818,6 @@ class MethodOutlook(MethodBase):
# Search emails using Microsoft Graph API # Search emails using Microsoft Graph API
try: try:
import requests
# Microsoft Graph API endpoint for searching messages # Microsoft Graph API endpoint for searching messages
graph_url = "https://graph.microsoft.com/v1.0" graph_url = "https://graph.microsoft.com/v1.0"
headers = { headers = {
@ -1087,8 +1000,6 @@ class MethodOutlook(MethodBase):
# List drafts using Microsoft Graph API # List drafts using Microsoft Graph API
try: try:
import requests
# Microsoft Graph API endpoint for messages # Microsoft Graph API endpoint for messages
graph_url = "https://graph.microsoft.com/v1.0" graph_url = "https://graph.microsoft.com/v1.0"
headers = { headers = {
@ -1208,8 +1119,6 @@ class MethodOutlook(MethodBase):
# Find drafts using Microsoft Graph API # Find drafts using Microsoft Graph API
try: try:
import requests
# Microsoft Graph API endpoint for messages # Microsoft Graph API endpoint for messages
graph_url = "https://graph.microsoft.com/v1.0" graph_url = "https://graph.microsoft.com/v1.0"
headers = { headers = {
@ -1301,8 +1210,6 @@ class MethodOutlook(MethodBase):
This is a helper method to identify which folder a draft is in This is a helper method to identify which folder a draft is in
""" """
try: try:
import requests
graph_url = "https://graph.microsoft.com/v1.0" graph_url = "https://graph.microsoft.com/v1.0"
headers = { headers = {
"Authorization": f"Bearer {connection['accessToken']}", "Authorization": f"Bearer {connection['accessToken']}",
@ -1347,8 +1254,6 @@ class MethodOutlook(MethodBase):
# Check Drafts folder directly # Check Drafts folder directly
try: try:
import requests
# Microsoft Graph API endpoint for messages # Microsoft Graph API endpoint for messages
graph_url = "https://graph.microsoft.com/v1.0" graph_url = "https://graph.microsoft.com/v1.0"
headers = { headers = {
@ -1613,12 +1518,11 @@ class MethodOutlook(MethodBase):
documents = [] documents = []
try: try:
if composition_documents: if composition_documents:
from modules.interfaces.interfaceChatModel import ChatDocument as ChatDoc
for d in composition_documents: for d in composition_documents:
try: try:
data = self.service.getFileData(d.fileId) if hasattr(d, 'fileId') else None data = self.service.getFileData(d.fileId) if hasattr(d, 'fileId') else None
if data: if data:
documents.append(ChatDoc(fileData=data, fileName=d.fileName, mimeType=d.mimeType)) documents.append(ChatDocument(fileData=data, fileName=d.fileName, mimeType=d.mimeType))
except Exception: except Exception:
continue continue
except Exception: except Exception:
@ -1642,7 +1546,6 @@ class MethodOutlook(MethodBase):
# Parse the AI response to ensure it's valid JSON # Parse the AI response to ensure it's valid JSON
try: try:
import json
# Clean the response and parse as JSON # Clean the response and parse as JSON
cleaned_response = composed_email.strip() cleaned_response = composed_email.strip()
if cleaned_response.startswith('```json'): if cleaned_response.startswith('```json'):

View file

@ -14,7 +14,7 @@ import aiohttp
import asyncio import asyncio
from modules.workflows.methods.methodBase import MethodBase, action from modules.workflows.methods.methodBase import MethodBase, action
from modules.interfaces.interfaceChatModel import ActionResult from modules.datamodels.datamodelWorkflow import ActionResult
from modules.shared.timezoneUtils import get_utc_timestamp from modules.shared.timezoneUtils import get_utc_timestamp
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -1,11 +1,11 @@
import logging import logging
import csv import csv
import io import io
import json as _json
from typing import Any, Dict from typing import Any, Dict
from modules.workflows.methods.methodBase import MethodBase, action from modules.workflows.methods.methodBase import MethodBase, action
from modules.interfaces.interfaceChatModel import ActionResult, ActionDocument from modules.datamodels.datamodelWorkflow import ActionResult, ActionDocument
from modules.interfaces.interfaceWebObjects import WebInterface from modules.datamodels.datamodelWeb import (
from modules.interfaces.interfaceWebModel import (
WebSearchRequest, WebSearchRequest,
WebCrawlRequest, WebCrawlRequest,
WebScrapeRequest, WebScrapeRequest,
@ -64,15 +64,16 @@ class MethodWeb(MethodBase):
include_raw_content=parameters.get("includeRawContent"), include_raw_content=parameters.get("includeRawContent"),
) )
# Perform request # Perform request via centralized service wrappers
web_interface = await WebInterface.create() web_search_result = await self.services.web.webSearch(web_search_request)
web_search_result = await web_interface.search(web_search_request)
# Convert search results to CSV format (generic) # Convert search results to CSV format (generic)
if web_search_result.success and web_search_result.documents: if web_search_result.success and web_search_result.documents:
csv_content = web_interface.convert_web_search_result_to_csv(web_search_result) csv_content = self._convert_web_result_to_csv(web_search_result)
csv_document = web_interface.create_csv_action_document( csv_document = ActionDocument(
csv_content, f"web_search_results.csv" documentName=f"web_search_results.csv",
documentData=csv_content,
mimeType="text/csv"
) )
return ActionResult(success=True, documents=[csv_document]) return ActionResult(success=True, documents=[csv_document])
else: else:
@ -254,9 +255,8 @@ class MethodWeb(MethodBase):
format=fmt, format=fmt,
) )
# Perform request # Perform request via centralized service wrappers
web_interface = await WebInterface.create() web_crawl_result = await self.services.web.webCrawl(web_crawl_request)
web_crawl_result = await web_interface.crawl(web_crawl_request)
# Convert and enrich with concise summaries per URL for better context # Convert and enrich with concise summaries per URL for better context
if web_crawl_result.success: if web_crawl_result.success:
@ -310,10 +310,12 @@ class MethodWeb(MethodBase):
json_content = _json.dumps(payload, ensure_ascii=False, indent=2) json_content = _json.dumps(payload, ensure_ascii=False, indent=2)
except Exception: except Exception:
# Fallback to original conversion # Fallback to original conversion
json_content = web_interface.convert_web_result_to_json(web_crawl_result) json_content = self._convert_web_result_to_json(web_crawl_result)
json_document = web_interface.create_json_action_document( json_document = ActionDocument(
json_content, f"web_crawl_results.json" documentName=f"web_crawl_results.json",
documentData=json_content,
mimeType="application/json"
) )
return ActionResult(success=True, documents=[json_document]) return ActionResult(success=True, documents=[json_document])
else: else:
@ -383,16 +385,16 @@ class MethodWeb(MethodBase):
format=fmt, format=fmt,
) )
# Perform request # Perform request via centralized service wrappers
web_interface = await WebInterface.create() web_scrape_result = await self.services.web.webScrape(web_scrape_request)
web_scrape_result = await web_interface.scrape(web_scrape_request)
# Convert to proper JSON format # Convert to proper JSON format
if web_scrape_result.success: if web_scrape_result.success:
json_content = web_interface.convert_web_result_to_json(web_scrape_result) json_content = self._convert_web_result_to_json(web_scrape_result)
json_document = web_interface.create_json_action_document( json_document = ActionDocument(
json_content, documentName=f"web_scrape_results.json",
f"web_scrape_results.json" documentData=json_content,
mimeType="application/json"
) )
return ActionResult( return ActionResult(
success=True, success=True,
@ -403,3 +405,36 @@ class MethodWeb(MethodBase):
except Exception as e: except Exception as e:
return ActionResult(success=False, error=str(e)) return ActionResult(success=False, error=str(e))
# Helpers
def _convert_web_result_to_json(self, web_result):
if not getattr(web_result, 'success', False) or not getattr(web_result, 'documents', None):
return _json.dumps({"success": getattr(web_result, 'success', False), "error": getattr(web_result, 'error', None)})
document_data = web_result.documents[0].documentData
result_dict = {
"success": True,
"results": [
{
"url": str(getattr(result, 'url', "")),
"content": getattr(result, 'content', "")
}
for result in getattr(document_data, 'results', [])
],
"total_count": getattr(document_data, 'total_count', 0)
}
if hasattr(document_data, 'urls'):
result_dict["urls"] = [str(url) for url in getattr(document_data, 'urls', [])]
elif hasattr(document_data, 'query'):
result_dict["query"] = getattr(document_data, 'query', None)
return _json.dumps(result_dict, indent=2, ensure_ascii=False)
def _convert_web_result_to_csv(self, web_search_result):
if not getattr(web_search_result, 'success', False) or not getattr(web_search_result, 'documents', None):
return ""
output = io.StringIO()
writer = csv.writer(output, delimiter=';')
writer.writerow(['url', 'title'])
document_data = web_search_result.documents[0].documentData
for result in getattr(document_data, 'results', []):
writer.writerow([str(getattr(result, 'url', "")), getattr(result, 'title', "")])
return output.getvalue()

View file

@ -4,7 +4,8 @@
import logging import logging
from typing import List from typing import List
from datetime import datetime, UTC from datetime import datetime, UTC
from modules.interfaces.interfaceChatModel import TaskStep, ActionResult from modules.datamodels.datamodelWorkflow import TaskStep
from modules.datamodels.datamodelWorkflow import ActionResult
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -7,9 +7,9 @@ import json
import time import time
from typing import Dict, Any, Optional, List, Union from typing import Dict, Any, Optional, List, Union
from datetime import datetime, UTC from datetime import datetime, UTC
from modules.interfaces.interfaceChatModel import ( from modules.datamodels.datamodelWorkflow import (TaskStep, TaskContext, ReviewResult, TaskPlan, WorkflowResult, TaskResult, ReviewContext)
TaskStatus, TaskStep, TaskContext, TaskAction, ReviewResult, TaskPlan, WorkflowResult, TaskResult, ReviewContext, ActionResult from modules.datamodels.datamodelWorkflow import TaskStatus, ActionResult
) from modules.datamodels.datamodelChat import ChatWorkflow, ChatMessage, ChatDocument
from modules.interfaces.interfaceAppObjects import getInterface as getAppObjects from modules.interfaces.interfaceAppObjects import getInterface as getAppObjects
from modules.shared.timezoneUtils import get_utc_timestamp from modules.shared.timezoneUtils import get_utc_timestamp
from modules.workflows.processing.executionState import TaskExecutionState from modules.workflows.processing.executionState import TaskExecutionState
@ -73,7 +73,7 @@ class HandlingTasks:
# Create proper context object for task planning # Create proper context object for task planning
# For task planning, we need to create a minimal TaskStep since TaskContext requires it # For task planning, we need to create a minimal TaskStep since TaskContext requires it
from modules.interfaces.interfaceChatModel import TaskStep from modules.datamodels.datamodelWorkflow import TaskStep
planning_task_step = TaskStep( planning_task_step = TaskStep(
id="planning", id="planning",
objective=userInput, objective=userInput,

View file

@ -7,7 +7,8 @@ import importlib
import pkgutil import pkgutil
import inspect import inspect
from typing import Any, Dict, List from typing import Any, Dict, List
from modules.interfaces.interfaceChatModel import TaskContext, ReviewContext, ChatDocument, DocumentExchange from modules.datamodels.datamodelWorkflow import TaskContext, ReviewContext, DocumentExchange
from modules.datamodels.datamodelChat import ChatDocument
from modules.services.serviceDocument.documentUtility import getFileExtension from modules.services.serviceDocument.documentUtility import getFileExtension
from modules.workflows.methods.methodBase import MethodBase from modules.workflows.methods.methodBase import MethodBase

View file

@ -6,10 +6,12 @@ import asyncio
from modules.interfaces.interfaceAppObjects import User from modules.interfaces.interfaceAppObjects import User
from modules.interfaces.interfaceChatModel import (UserInputRequest, ChatMessage, ChatWorkflow, TaskItem, TaskStatus, ChatDocument) from modules.datamodels.datamodelWorkflow import UserInputRequest
from modules.datamodels.datamodelChat import ChatMessage, ChatWorkflow, ChatDocument
from modules.datamodels.datamodelWorkflow import TaskItem, TaskStatus
from modules.interfaces.interfaceChatObjects import ChatObjects from modules.interfaces.interfaceChatObjects import ChatObjects
from modules.workflows.processing.handlingTasks import HandlingTasks, WorkflowStoppedException from modules.workflows.processing.handlingTasks import HandlingTasks, WorkflowStoppedException
from modules.interfaces.interfaceChatModel import WorkflowResult from modules.datamodels.datamodelWorkflow import WorkflowResult
from modules.shared.timezoneUtils import get_utc_timestamp from modules.shared.timezoneUtils import get_utc_timestamp
import uuid import uuid
@ -239,7 +241,7 @@ class WorkflowManager:
logger.info(f"Task {current_task_index}/{total_tasks}: {task_step.objective}") logger.info(f"Task {current_task_index}/{total_tasks}: {task_step.objective}")
# Build TaskContext (mode-specific behavior is inside HandlingTasks) # Build TaskContext (mode-specific behavior is inside HandlingTasks)
from modules.interfaces.interfaceChatModel import TaskContext from modules.datamodels.datamodelWorkflow import TaskContext
task_context = TaskContext( task_context = TaskContext(
task_step=task_step, task_step=task_step,
workflow=workflow, workflow=workflow,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -1 +0,0 @@
# noqa

View file

File diff suppressed because one or more lines are too long

View file

@ -1,248 +0,0 @@
"""Tests for method web.py"""
import json
import logging
import pytest
from unittest.mock import patch
from modules.workflows.methods.methodWeb import MethodWeb
from tests.fixtures.tavily_responses import (
RESPONSE_SEARCH_HOW_OLD_IS_EARTH_NO_ANSWER,
RESPONSE_EXTRACT_HOW_OLD_IS_EARTH_NO_ANSWER,
)
logger = logging.getLogger(__name__)
@pytest.mark.asyncio
@pytest.mark.expensive
async def test_method_web_search_live():
"""Tests method web search with live API calls."""
logger.info("=" * 50)
logger.info("==> Test: Method Web Search Live")
method_web = MethodWeb(serviceCenter=None)
# Actual request
action_result = await method_web.search(
{"query": "How old is the earth", "maxResults": 5}
)
# Evaluate results
assert action_result.success
assert len(action_result.documents) > 0
logger.info(f"Action result success status: {action_result.success}")
logger.info(f"Action result error: {action_result.error}")
logger.info(f"Action result label: {action_result.resultLabel}")
logger.info("Documents:")
for doc in action_result.documents:
logger.info(f" - Document Name: {doc.documentName}")
logger.info(f" --> Document Mime Type: {doc.mimeType}")
logger.info(f" --> Document Data: {doc.documentData}")
@pytest.mark.asyncio
async def test_method_web_search_dummy():
"""Tests method web search with dummy response data - no external API calls."""
logger.info("=" * 50)
logger.info("==> Test: Method Web Search Dummy")
method_web = MethodWeb(serviceCenter=None)
# Mock the Tavily API response
with patch(
"tavily.AsyncTavilyClient.search",
return_value=RESPONSE_SEARCH_HOW_OLD_IS_EARTH_NO_ANSWER,
) as mock_client:
action_result = await method_web.search(
{"query": "How old is the earth", "maxResults": 5}
)
mock_client.assert_called_once()
# Evaluate results
assert action_result.success
assert len(action_result.documents) > 0
logger.info(f"Action result success status: {action_result.success}")
logger.info(f"Action result error: {action_result.error}")
logger.info(f"Action result label: {action_result.resultLabel}")
logger.info("Documents:")
for doc in action_result.documents:
logger.info(f" - Document Name: {doc.documentName}")
logger.info(f" --> Document Mime Type: {doc.mimeType}")
logger.info(f" --> Document Data: {doc.documentData}")
@pytest.mark.asyncio
@pytest.mark.expensive
async def test_method_web_crawl_live():
"""Tests method web crawl with live API calls."""
logger.info("=" * 50)
logger.info("==> Test: Method Web Crawl Live")
method_web = MethodWeb(serviceCenter=None)
# Create mock document data with URLs from search results
search_results_json = {
"documentData": {
"results": [
{"url": "https://en.wikipedia.org/wiki/Age_of_Earth"},
{"url": "https://www.planetary.org/articles/how-old-is-the-earth"},
]
}
}
# Mock the service center methods
with patch.object(method_web, "service") as mock_service:
mock_service.getChatDocumentsFromDocumentList.return_value = [
type("MockDoc", (), {"fileId": "test-file-id", "fileName": "test-search-results.json"})()
]
mock_service.getFileData.return_value = json.dumps(search_results_json).encode(
"utf-8"
)
# Actual request
action_result = await method_web.crawl({"documentList": "test-document-list-ref"})
# Evaluate results
assert action_result.success
assert len(action_result.documents) > 0
logger.info(f"Action result success status: {action_result.success}")
logger.info(f"Action result error: {action_result.error}")
logger.info(f"Action result label: {action_result.resultLabel}")
logger.info("Documents:")
for doc in action_result.documents:
logger.info(f" - Document Name: {doc.documentName}")
logger.info(f" --> Document Mime Type: {doc.mimeType}")
logger.info(f" --> Document Data: {doc.documentData}")
@pytest.mark.asyncio
async def test_method_web_crawl_dummy():
"""Tests method web crawl with dummy response data - no external API calls."""
logger.info("=" * 50)
logger.info("==> Test: Method Web Crawl Dummy")
method_web = MethodWeb(serviceCenter=None)
# Create mock document data with URLs from search results
search_results_json = {
"documentData": {
"results": [
{"url": "https://en.wikipedia.org/wiki/Age_of_Earth"},
{"url": "https://www.planetary.org/articles/how-old-is-the-earth"},
]
}
}
# Mock both the service center and Tavily API
with (
patch.object(method_web, "service") as mock_service,
patch(
"tavily.AsyncTavilyClient.extract",
return_value=RESPONSE_EXTRACT_HOW_OLD_IS_EARTH_NO_ANSWER,
) as mock_client,
):
mock_service.getChatDocumentsFromDocumentList.return_value = [
type("MockDoc", (), {"fileId": "test-file-id", "fileName": "test-search-results.json"})()
]
mock_service.getFileData.return_value = json.dumps(search_results_json).encode(
"utf-8"
)
action_result = await method_web.crawl({"documentList": "test-document-list-ref"})
mock_client.assert_called_once()
# Evaluate results
assert action_result.success
assert len(action_result.documents) > 0
logger.info(f"Action result success status: {action_result.success}")
logger.info(f"Action result error: {action_result.error}")
logger.info(f"Action result label: {action_result.resultLabel}")
logger.info("Documents:")
for doc in action_result.documents:
logger.info(f" - Document Name: {doc.documentName}")
logger.info(f" --> Document Mime Type: {doc.mimeType}")
logger.info(f" --> Document Data: {doc.documentData}")
@pytest.mark.asyncio
@pytest.mark.expensive
async def test_method_web_scrape_live():
"""Tests method web scrape with live API calls."""
logger.info("=" * 50)
logger.info("==> Test: Method Web Scrape Live")
method_web = MethodWeb(serviceCenter=None)
# Actual request
action_result = await method_web.scrape(
{"query": "How old is the earth", "maxResults": 3}
)
# Evaluate results
assert action_result.success
assert len(action_result.documents) > 0
logger.info(f"Action result success status: {action_result.success}")
logger.info(f"Action result error: {action_result.error}")
logger.info(f"Action result label: {action_result.resultLabel}")
logger.info("Documents:")
for doc in action_result.documents:
logger.info(f" - Document Name: {doc.documentName}")
logger.info(f" --> Document Mime Type: {doc.mimeType}")
logger.info(f" --> Document Data: {doc.documentData}")
@pytest.mark.asyncio
async def test_method_web_scrape_dummy():
"""Tests method web scrape with dummy response data - no external API calls."""
logger.info("=" * 50)
logger.info("==> Test: Method Web Scrape Dummy")
method_web = MethodWeb(serviceCenter=None)
# Mock both Tavily API responses (search + extract)
with (
patch(
"tavily.AsyncTavilyClient.search",
return_value=RESPONSE_SEARCH_HOW_OLD_IS_EARTH_NO_ANSWER,
) as mock_search,
patch(
"tavily.AsyncTavilyClient.extract",
return_value=RESPONSE_EXTRACT_HOW_OLD_IS_EARTH_NO_ANSWER,
) as mock_extract,
):
action_result = await method_web.scrape(
{"query": "How old is the earth", "maxResults": 3}
)
mock_search.assert_called_once()
mock_extract.assert_called_once()
# Evaluate results
assert action_result.success
assert len(action_result.documents) > 0
logger.info(f"Action result success status: {action_result.success}")
logger.info(f"Action result error: {action_result.error}")
logger.info(f"Action result label: {action_result.resultLabel}")
logger.info("Documents:")
for doc in action_result.documents:
logger.info(f" - Document Name: {doc.documentName}")
logger.info(f" --> Document Mime Type: {doc.mimeType}")
logger.info(f" --> Document Data: {doc.documentData}")

View file

@ -1,311 +0,0 @@
#!/usr/bin/env python3
"""
Simple test script for Microsoft Graph Search API
Tests folder search queries directly
"""
import requests
import json
import sys
import os
# Add the gateway modules to the path
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
def test_graph_folders_direct(access_token):
"""Test direct Microsoft Graph API call to list folders"""
print("🔍 Testing direct Graph API folder listing...")
# Try to list folders from the main site - need to get site ID first
# Let's try to find the site by name first
url = "https://graph.microsoft.com/v1.0/sites/pcuster.sharepoint.com:/sites/SSSRESYNachfolge:/drive/root/children"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
items = data.get('value', [])
print(f"✅ SUCCESS - Found {len(items)} items in root")
folders = []
files = []
for item in items:
if 'folder' in item:
folders.append(item)
elif 'file' in item:
files.append(item)
print(f" 📁 Folders: {len(folders)}")
print(f" 📄 Files: {len(files)}")
if folders:
print("\n📁 FOLDERS found:")
for i, folder in enumerate(folders[:5], 1):
name = folder.get('name', 'No name')
web_url = folder.get('webUrl', 'No URL')
print(f" {i}. {name}")
print(f" URL: {web_url}")
print()
else:
print(f"❌ ERROR - Status {response.status_code}")
print(f"Error: {response.text[:200]}")
except Exception as e:
print(f"Exception: {str(e)}")
def test_graph_search(access_token, query_string):
"""Test a Microsoft Graph Search API query and show resulting paths"""
url = "https://graph.microsoft.com/v1.0/search/query"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
payload = {
"requests": [
{
"entityTypes": ["driveItem"],
"query": {
"queryString": query_string
},
"from": 0,
"size": 50
}
]
}
print(f"Testing: {query_string}")
print("-" * 50)
try:
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
data = response.json()
# Extract useful info
if "value" in data and len(data["value"]) > 0:
hits = data["value"][0].get("hitsContainers", [])
if hits:
total = hits[0].get("total", 0)
results = hits[0].get("hits", [])
print(f"✅ SUCCESS - Found {total} results")
# First, let's see what types of results we're getting
print(f"📊 Analyzing {len(results)} results...")
# Count different types of results with better detection
file_count = 0
folder_count = 0
other_count = 0
# Debug: Let's see what the actual resource structure looks like
if results:
print("🔍 DEBUG: First result structure:")
first_result = results[0]
print(f" Keys: {list(first_result.keys())}")
if 'resource' in first_result:
resource = first_result['resource']
print(f" Resource keys: {list(resource.keys())}")
if 'folder' in resource:
print(f" Folder info: {resource['folder']}")
if 'file' in resource:
print(f" File info: {resource['file']}")
print()
for result in results:
resource = result.get('resource', {})
# Better detection logic
is_folder = False
is_file = False
# Check for explicit folder/file indicators
if 'folder' in resource:
is_folder = True
elif 'file' in resource:
is_file = True
else:
# Try to detect by URL pattern or other indicators
web_url = resource.get('webUrl', '')
name = resource.get('name', '')
# Check if URL ends with a file extension (likely a file)
if '.' in name and any(name.lower().endswith(ext) for ext in ['.pdf', '.docx', '.xlsx', '.pptx', '.txt', '.cs', '.py', '.js', '.html', '.css']):
is_file = True
# Check if URL has no file extension and looks like a folder path
elif '.' not in name and ('/' in web_url or '\\' in web_url):
is_folder = True
if is_folder:
folder_count += 1
elif is_file:
file_count += 1
else:
other_count += 1
print(f" 📄 Files: {file_count}")
print(f" 📁 Folders: {folder_count}")
print(f" ❓ Other: {other_count}")
print()
# Show sample results regardless of type
print(f"📋 Sample results (showing first 5):")
for i, result in enumerate(results[:5], 1):
resource = result.get('resource', {})
web_url = resource.get('webUrl', 'No URL')
name = resource.get('name', 'No name')
# Determine type using same logic as counting
is_folder = False
is_file = False
if 'folder' in resource:
is_folder = True
elif 'file' in resource:
is_file = True
else:
# Try to detect by URL pattern or other indicators
web_url = resource.get('webUrl', '')
name = resource.get('name', '')
# Check if URL ends with a file extension (likely a file)
if '.' in name and any(name.lower().endswith(ext) for ext in ['.pdf', '.docx', '.xlsx', '.pptx', '.txt', '.cs', '.py', '.js', '.html', '.css']):
is_file = True
# Check if URL has no file extension and looks like a folder path
elif '.' not in name and ('/' in web_url or '\\' in web_url):
is_folder = True
if is_folder:
item_type = "📁 FOLDER"
elif is_file:
file_info = resource.get('file', {})
mime_type = file_info.get('mimeType', 'Unknown type') if file_info else 'Detected by extension'
item_type = f"📄 FILE ({mime_type})"
else:
item_type = "❓ UNKNOWN"
# Extract path from webUrl
if '/sites/SSSRESYNachfolge/' in web_url:
path_part = web_url.split('/sites/SSSRESYNachfolge/')[-1]
path_with_backslashes = path_part.replace('/', '\\')
display_path = f"\\{path_with_backslashes}"
else:
display_path = web_url
print(f" {i}. {item_type} - {name}")
print(f" Path: {display_path}")
print(f" URL: {web_url}")
print()
if len(results) > 5:
print(f" ... and {len(results) - 5} more results")
# Now filter and show only FOLDER results if any exist
folder_results = []
for result in results:
resource = result.get('resource', {})
# Use the same detection logic as counting
is_folder = False
if 'folder' in resource:
is_folder = True
else:
# Try to detect by URL pattern or other indicators
web_url = resource.get('webUrl', '')
name = resource.get('name', '')
# Check if URL has no file extension and looks like a folder path
if '.' not in name and ('/' in web_url or '\\' in web_url):
is_folder = True
if is_folder:
folder_results.append(result)
if folder_results:
print(f"\n📁 FOLDER DETAILS ({len(folder_results)} folders found):")
for i, result in enumerate(folder_results, 1):
web_url = result.get('resource', {}).get('webUrl', 'No URL')
name = result.get('resource', {}).get('name', 'No name')
if '/sites/SSSRESYNachfolge/' in web_url:
path_part = web_url.split('/sites/SSSRESYNachfolge/')[-1]
path_with_backslashes = path_part.replace('/', '\\')
folder_path = f"\\{path_with_backslashes}"
else:
folder_path = web_url
print(f" {i}. 📁 {name}")
print(f" Path: {folder_path}")
print(f" URL: {web_url}")
print()
else:
print(f"\n❌ No folders found in results - all {total} results are files or other types")
else:
print("❌ SUCCESS but no hits containers found")
else:
print("❌ SUCCESS but no value array in response")
else:
print(f"❌ ERROR - Status {response.status_code}")
error_text = response.text[:200] + "..." if len(response.text) > 200 else response.text
print(f"Error: {error_text}")
except Exception as e:
print(f"Exception: {str(e)}")
def main():
"""Main test function"""
# Use the access token from the database
access_token = "eyJ0eXAiOiJKV1QiLCJub25jZSI6IkxwTjBjTXo2SGlja2ZPLUpnekRwTFE1QktfQmVOWHBwRWZ2UzZBMDh2REUiLCJhbGciOiJSUzI1NiIsIng1dCI6IkpZaEFjVFBNWl9MWDZEQmxPV1E3SG4wTmVYRSIsImtpZCI6IkpZaEFjVFBNWl9MWDZEQmxPV1E3SG4wTmVYRSJ9.eyJhdWQiOiIwMDAwMDAwMy0wMDAwLTAwMDAtYzAwMC0wMDAwMDAwMDAwMDAiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC82YTUxYWFlYi0yNDY3LTQxODYtOTUwNC0yYTA1YWVkYzU5MWYvIiwiaWF0IjoxNzU3MDEwNTc0LCJuYmYiOjE3NTcwMTA1NzQsImV4cCI6MTc1NzAxNTQ1MSwiYWNjdCI6MCwiYWNyIjoiMSIsImFjcnMiOlsicDEiXSwiYWlvIjoiQVpRQWEvOFpBQUFBcU0xNVFOMkhaQld5QXNsbStiT0QzbzRuU1RhUzg5bGdTV3ZUQVZvYVhqcUhlT1VaNFE1aEh0bE51WUdxelEvM0tDRnZlZktycU1HTUp2VmlVaWVibUhjbnBtL0FaRFA1Sk1YNnI4c1FCSVdLVTZPY29sUUNuOWpvcVZLb1VIOFl3WTJhM3picTlkeGdqVC94dU5NaCtKcXhMV1JMdEUrUjBZeGl0c3J0QXhpd0pRaGZmalIzK0xPSGtmVkxhOExaIiwiYW1yIjpbInB3ZCIsIm1mYSJdLCJhcHBfZGlzcGxheW5hbWUiOiJQb3dlck9uIEFwcCIsImFwcGlkIjoiYzdlNzExMmQtNjFkYy00ZjNhLThjZDMtMDhjYzRjZDc1MDRjIiwiYXBwaWRhY3IiOiIxIiwiZmFtaWx5X25hbWUiOiJNb3RzY2giLCJnaXZlbl9uYW1lIjoiUGF0cmljayIsImlkdHlwIjoidXNlciIsImlwYWRkciI6IjE3OC4xOTcuMjE4LjQ4IiwibmFtZSI6IlBhdHJpY2sgTW90c2NoIiwib2lkIjoiN2QwOGFhYjktYTE3MC00OTc1LTg4OTgtYmM3ZTBhOTU0ODhlIiwicGxhdGYiOiIzIiwicHVpZCI6IjEwMDM3RkZFOENERDZBODIiLCJyaCI6IjEuQVFzQTY2cFJhbWNraGtHVkJDb0ZydHhaSHdNQUFBQUFBQUFBd0FBQUFBQUFBQUNFQURBTEFBLiIsInNjcCI6IkZpbGVzLlJlYWRXcml0ZS5BbGwgTWFpbC5SZWFkV3JpdGUgTWFpbC5SZWFkV3JpdGUuU2hhcmVkIE1haWwuU2VuZCBvcGVuaWQgcHJvZmlsZSBTaXRlcy5SZWFkV3JpdGUuQWxsIFVzZXIuUmVhZCBlbWFpbCIsInNpZCI6IjAwNmY5Mjk5LTY3ZDUtYmU3Zi1kYWI4LWQwYTBlZTI1MTBkNiIsInNpZ25pbl9zdGF0ZSI6WyJrbXNpIl0sInN1YiI6IklnMGlwM3hhZGJMaXVLemJGZ3dWaE5JTV9Eekcwd3B4aUVGYjJKWXVjbjQiLCJ0ZW5hbnRfcmVnaW9uX3Njb3BlIjoiRVUiLCJ0aWQiOiI2YTUxYWFlYi0yNDY3LTQxODYtOTUwNC0yYTA1YWVkYzU5MWYiLCJ1bmlxdWVfbmFtZSI6InAubW90c2NoQHZhbHVlb24uY2giLCJ1cG4iOiJwLm1vdHNjaEB2YWx1ZW9uLmNoIiwidXRpIjoieTh5ZGhEcWRDMG1nVTBpLV94azFBUSIsInZlciI6IjEuMCIsIndpZHMiOlsiOWI4OTVkOTItMmNkMy00NGM3LTlkMDItYTZhYzJkNWVhNWMzIiwiY2YxYzM4ZTUtMzYyMS00MDA0LWE3Y2ItODc5NjI0ZGNlZDdjIiwiMTU4YzA0N2EtYzkwNy00NTU2LWI3ZWYtNDQ2NTUxYTZiNWY3IiwiODkyYzU4NDItYTlhNi00NjNhLTgwNDEtNzJhYTA4Y2EzY2Y2IiwiOWYwNjIwNGQtNzNjMS00ZDRjLTg4MGEtNmVkYjkwNjA2ZmQ4IiwiYjc5ZmJmNGQtM2VmOS00Njg5LTgxNDMtNzZiMTk0ZTg1NTA5Il0sInhtc19mdGQiOiIwcEZ4RVctQnl6Y3M5UW5HdXNDbU1Ka1V4MHNQWlEzOUkzWUwxRGZJdnpzQmMzZGxaR1Z1WXkxa2MyMXoiLCJ4bXNfaWRyZWwiOiIxIDI0IiwieG1zX3N0Ijp7InN1YiI6IlIydkQwRzFtbWFZUkM3SllXY0lTWlcyS0RQZ05CakJMRmw2ZUxBQl9QVU0ifSwieG1zX3RjZHQiOjE0MTgyMTQ1MDEsInhtc190ZGJyIjoiRVUifQ.JYEWH2YxBrgWSn-9WN3BixJ91q19RGd0U7HgiiLpmwKUicft8zrovO8wKVU5rkly6CBcEO_eGAvyqQHSjFLHXKGDrutrFVdLTLB0vUu3J1Lkw31CiJF_y6Y3r2VytOF8evcYwh_Ye-5eoAxIr5avR8j_T51RPkLG53QSJ-tA5utDgHGWa65T5-mmeZxI-ThYxfyLori1uS8TSchJBdwrWwv8pkklHn6lZrFfgiuviRjLrOOLVUL_fzIod_eOKjo31YHhUzfm-QD3vvQkqnWNcdQ4D0UaTxKW291fHFafQZ9SkH9m0BD9nn56QBqijUBhvA8qMZC_cObb3DpR0GR_xA"
print("=" * 60)
print("Microsoft Graph API Test Suite")
print("=" * 60)
# First test: Direct folder listing (should work better than search)
print("\nTEST 0: Direct Graph API folder listing")
test_graph_folders_direct(access_token)
# Test different query types to find both files and folders
test_queries = [
# Test 1: Test with Venus folder (empty folder created for testing)
"Venus",
# Test 2: Folder-specific searches for Venus
"kind:folder AND Venus",
# Test 3: Original specific query (found 8 results - all files)
"Druckersteuerung AND Eskalation AND Logobject",
# Test 4: Broader folder-focused queries
"Druckersteuerung",
"Eskalation",
"Logobject",
# Test 5: Folder-specific searches
"kind:folder AND Druckersteuerung",
"kind:folder AND Eskalation",
# Test 6: General folder search to see what folders exist
"kind:folder",
]
for i, query in enumerate(test_queries, 1):
print(f"\nTEST {i}: {query}")
test_graph_search(access_token, query)
print()
if __name__ == "__main__":
main()

View file

@ -1,263 +0,0 @@
"""
DSGVO-konformer Daten-Neutralisierer für KI-Agentensysteme
Unterstützt TXT, JSON, CSV, Excel und Word-Dateien
Mehrsprachig: DE, EN, FR, IT
"""
import os
import shutil
import logging
import csv
import json
import pandas as pd
from datetime import datetime
from pathlib import Path
from neutralizer import DataAnonymizer
import traceback
# Define directories
SCRIPT_DIR = Path(__file__).parent
INPUT_DIR = SCRIPT_DIR / 'input'
OUTPUT_DIR = SCRIPT_DIR / 'output'
LOG_DIR = SCRIPT_DIR / 'logs'
LOG_MAPPING = LOG_DIR / 'log_mapping.csv'
LOG_REPLACEMENTS = LOG_DIR / 'log_replacements.csv'
# Configure logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
def setup_directories():
"""Setup output and log directories"""
# Close any existing log handlers
for handler in logger.handlers[:]:
handler.close()
logger.removeHandler(handler)
# Clear and recreate output directory
output_dir = Path("output")
if output_dir.exists():
shutil.rmtree(output_dir)
output_dir.mkdir()
logger.info(f"Output directory '{output_dir}' created")
# Clear and recreate logs directory
log_dir = Path("logs")
if log_dir.exists():
shutil.rmtree(log_dir)
log_dir.mkdir()
logger.info(f"Log directory '{log_dir}' created")
# Create log files
mapping_log = log_dir / "log_mapping.csv"
replacements_log = log_dir / "log_replacements.csv"
# Create headers for mapping log
with open(mapping_log, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['timestamp', 'success', 'file_name', 'replaced_fields', 'content_type', 'headers', 'row_count'])
# Create headers for replacements log
with open(replacements_log, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['timestamp', 'success', 'file_name', 'original', 'replacement'])
# Reconfigure logging with new log file
for handler in logger.handlers[:]:
handler.close()
logger.removeHandler(handler)
file_handler = logging.FileHandler(LOG_DIR / 'app.log', encoding='utf-8')
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logger.addHandler(file_handler)
return output_dir, log_dir
def log_mapping(log_dir: Path, file_name: str, success: bool, replaced_fields: list):
"""Log mapping information"""
try:
with open(log_dir / "log_mapping.csv", 'a', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow([
datetime.now().isoformat(),
file_name,
success,
';'.join(replaced_fields) if replaced_fields else '',
'unknown'
])
except Exception as e:
logger.error(f"Error logging mapping: {str(e)}")
def log_replacements(log_dir: Path, file_name: str, mapping: dict):
"""Log replacement information"""
try:
with open(log_dir / "log_replacements.csv", 'a', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
for original, replacement in mapping.items():
writer.writerow([
datetime.now().isoformat(),
file_name,
original,
replacement
])
except Exception as e:
logger.error(f"Error logging replacements: {str(e)}")
def save_anonymized_data(data: any, output_path: Path, file_type: str):
"""Save anonymized data to file"""
try:
if file_type == '.csv':
if isinstance(data, pd.DataFrame):
data.to_csv(output_path, index=False, encoding='utf-8')
else:
raise ValueError("Data must be a DataFrame for CSV output")
elif file_type == '.json':
if isinstance(data, pd.DataFrame):
data.to_json(output_path, orient='records', force_ascii=False)
else:
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
elif file_type == '.xml':
if isinstance(data, str):
with open(output_path, 'w', encoding='utf-8') as f:
f.write(data)
else:
raise ValueError("Data must be a string for XML output")
elif file_type in ['.txt', '.docx']:
if isinstance(data, str):
with open(output_path, 'w', encoding='utf-8') as f:
f.write(data)
else:
raise ValueError("Data must be a string for text output")
else:
raise ValueError(f"Unsupported file type: {file_type}")
except Exception as e:
logger.error(f"Error saving anonymized data to {output_path}: {str(e)}")
raise
def read_file_content(file_path: Path) -> tuple[str, str]:
"""Read file content and determine content type"""
try:
file_type = file_path.suffix.lower()
if file_type == '.docx':
import docx
doc = docx.Document(file_path)
content = '\n'.join(para.text for para in doc.paragraphs)
else:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
return content, file_type
except Exception as e:
logger.error(f"Error reading file {file_path}: {str(e)}")
raise
def process_file(file_path: Path, anonymizer: DataAnonymizer) -> bool:
"""Process a single file and save anonymized version"""
try:
# Read file content
content = file_path.read_text(encoding='utf-8')
# Process content
result = anonymizer.process_content(content, file_path.suffix[1:])
if result.data is None:
logger.error(f"Failed to process {file_path.name}")
return False
# Save anonymized content with neutralized_ prefix
output_path = OUTPUT_DIR / f"neutralized_{file_path.name}"
if file_path.suffix.lower() == '.csv':
result.data.to_csv(output_path, index=False)
elif file_path.suffix.lower() == '.json':
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(result.data, f, indent=2)
elif file_path.suffix.lower() == '.xml':
with open(output_path, 'w', encoding='utf-8') as f:
f.write(result.data)
else:
# For text files, preserve original whitespace
with open(output_path, 'w', encoding='utf-8') as f:
f.write(result.data)
# Log processing details
timestamp = datetime.now().isoformat()
success = True
# Create detailed log entry
log_entry = {
'timestamp': timestamp,
'success': success,
'file_name': file_path.name,
'replaced_fields': ';'.join(set(result.replaced_fields)), # Use set to remove duplicates
'content_type': result.processed_info.get('type', 'unknown')
}
# Add type-specific details
if result.processed_info['type'] == 'table':
log_entry.update({
'headers': ';'.join(result.processed_info['headers']),
'row_count': result.processed_info['row_count']
})
elif result.processed_info['type'] == 'text':
tables = result.processed_info.get('tables', [])
text_sections = result.processed_info.get('text_sections', [])
log_entry.update({
'headers': '', # Empty for text files
'row_count': sum(s['length'] for s in text_sections) # Total text length
})
# Write to log file
with open(LOG_MAPPING, 'a', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=log_entry.keys())
if f.tell() == 0: # Write header if file is empty
writer.writeheader()
writer.writerow(log_entry)
# Log replacements
for original, replacement in result.mapping.items():
with open(LOG_REPLACEMENTS, 'a', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow([timestamp, success, file_path.name, original, replacement])
return True
except Exception as e:
logger.error(f"Error processing {file_path.name}: {str(e)}")
logger.debug(traceback.format_exc())
return False
def main():
# Setup directories
setup_directories()
# Initialize anonymizer
anonymizer = DataAnonymizer()
# Process all files
logger.info("Starting file processing...")
testdata_dir = Path("testdata")
for file_path in testdata_dir.glob("*.*"):
try:
logger.info(f"Processing file: {file_path.name}")
# Process file
if process_file(file_path, anonymizer):
logger.info(f"Anonymization completed for {file_path.name}")
else:
logger.error(f"Error processing {file_path.name}")
except Exception as e:
logger.error(f"Error processing {file_path.name}: {str(e)}")
continue
logger.info("Processing completed!")
if __name__ == "__main__":
main()

View file

@ -1,17 +0,0 @@
timestamp,success,file_name,replaced_fields,content_type,headers,row_count
2025-06-07T18:12:08.019661,True,Case.md,,text,,0
2025-06-07T18:12:08.040653,True,customers.csv,ahv_number;phone;credit_card;address;name;iban;email,table,id;name;email;phone;address;iban;credit_card;ahv_number,5
2025-06-07T18:12:08.064201,True,cv_lara_meier.txt,,text,,0
2025-06-07T18:12:08.080961,True,employees.csv,bank_account;first_name;last_name;office_address;phone;uid_number;email,table,employee_id;first_name;last_name;email;phone;department;office_address;uid_number;bank_account,5
2025-06-07T18:12:08.101660,True,english.txt,,text,,0
2025-06-07T18:12:08.125634,True,example.json,,json
2025-06-07T18:12:08.157397,True,example.xml,,xml
2025-06-07T18:12:08.178030,True,french.txt,,text,,0
2025-06-07T18:12:08.201071,True,german.txt,,text,,0
2025-06-07T18:12:08.228652,True,geschaeftsstrategie.txt,,text,,0
2025-06-07T18:12:08.255652,True,geschäfte.csv,datum;kundenname;kundenemail;zahlungsdetails;lieferadresse,table,geschäft_id;datum;kundenname;kundenemail;betrag;zahlungsmethode;zahlungsdetails;lieferadresse,5
2025-06-07T18:12:08.284172,True,italian.txt,,text,,0
2025-06-07T18:12:08.314527,True,kunden.csv,kreditkarte;vorname;adresse;telefon;iban;steuernummer;email;nachname,table,kunden_id;vorname;nachname;email;telefon;adresse;iban;kreditkarte;steuernummer,5
2025-06-07T18:12:08.349750,True,mitarbeiter.csv,vorname;büroadresse;telefon;steuernummer;email;sozialversicherungsnummer;nachname,table,mitarbeiter_id;vorname;nachname;email;telefon;abteilung;büroadresse;steuernummer;sozialversicherungsnummer,5
2025-06-07T18:12:08.389324,True,swiss.txt,,text,,0
2025-06-07T18:12:08.431916,True,transactions.csv,date;customer_email;payment_details;customer_name;shipping_address,table,transaction_id;date;customer_name;customer_email;amount;payment_method;payment_details;shipping_address,5
1 timestamp,success,file_name,replaced_fields,content_type,headers,row_count
2 2025-06-07T18:12:08.019661,True,Case.md,,text,,0
3 2025-06-07T18:12:08.040653,True,customers.csv,ahv_number;phone;credit_card;address;name;iban;email,table,id;name;email;phone;address;iban;credit_card;ahv_number,5
4 2025-06-07T18:12:08.064201,True,cv_lara_meier.txt,,text,,0
5 2025-06-07T18:12:08.080961,True,employees.csv,bank_account;first_name;last_name;office_address;phone;uid_number;email,table,employee_id;first_name;last_name;email;phone;department;office_address;uid_number;bank_account,5
6 2025-06-07T18:12:08.101660,True,english.txt,,text,,0
7 2025-06-07T18:12:08.125634,True,example.json,,json
8 2025-06-07T18:12:08.157397,True,example.xml,,xml
9 2025-06-07T18:12:08.178030,True,french.txt,,text,,0
10 2025-06-07T18:12:08.201071,True,german.txt,,text,,0
11 2025-06-07T18:12:08.228652,True,geschaeftsstrategie.txt,,text,,0
12 2025-06-07T18:12:08.255652,True,geschäfte.csv,datum;kundenname;kundenemail;zahlungsdetails;lieferadresse,table,geschäft_id;datum;kundenname;kundenemail;betrag;zahlungsmethode;zahlungsdetails;lieferadresse,5
13 2025-06-07T18:12:08.284172,True,italian.txt,,text,,0
14 2025-06-07T18:12:08.314527,True,kunden.csv,kreditkarte;vorname;adresse;telefon;iban;steuernummer;email;nachname,table,kunden_id;vorname;nachname;email;telefon;adresse;iban;kreditkarte;steuernummer,5
15 2025-06-07T18:12:08.349750,True,mitarbeiter.csv,vorname;büroadresse;telefon;steuernummer;email;sozialversicherungsnummer;nachname,table,mitarbeiter_id;vorname;nachname;email;telefon;abteilung;büroadresse;steuernummer;sozialversicherungsnummer,5
16 2025-06-07T18:12:08.389324,True,swiss.txt,,text,,0
17 2025-06-07T18:12:08.431916,True,transactions.csv,date;customer_email;payment_details;customer_name;shipping_address,table,transaction_id;date;customer_name;customer_email;amount;payment_method;payment_details;shipping_address,5

File diff suppressed because it is too large Load diff

View file

@ -1,334 +0,0 @@
"""
DSGVO-konformer Daten-Neutralisierer für KI-Agentensysteme
Unterstützt TXT, JSON, CSV, Excel und Word-Dateien
Mehrsprachig: DE, EN, FR, IT
"""
import re
import json
import pandas as pd
from typing import Dict, List, Tuple, Any, Union, Optional
from dataclasses import dataclass
import logging
import traceback
import xml.etree.ElementTree as ET
from io import StringIO
from patterns import Pattern, HeaderPatterns, DataPatterns, get_pattern_for_header, find_patterns_in_text, TextTablePatterns
# Configure logging
logger = logging.getLogger(__name__)
@dataclass
class TableData:
"""Repräsentiert Tabellendaten"""
headers: List[str]
rows: List[List[str]]
source_type: str # 'csv', 'json', 'xml', 'text_table'
@dataclass
class PlainText:
"""Repräsentiert normalen Text"""
content: str
source_type: str # 'txt', 'docx', 'text_plain'
@dataclass
class ProcessResult:
"""Result of content processing"""
data: Any
mapping: Dict[str, str]
replaced_fields: List[str]
processed_info: Dict[str, Any] # Additional processing information
class DataAnonymizer:
"""Hauptklasse für die Datenanonymisierung"""
def __init__(self):
"""Initialize the anonymizer with patterns"""
self.header_patterns = HeaderPatterns.patterns
self.data_patterns = DataPatterns.patterns
self.replaced_fields = set()
self.mapping = {}
self.processing_info = []
def _normalize_whitespace(self, text: str) -> str:
"""Normalize whitespace in text"""
text = re.sub(r'\s+', ' ', text)
text = text.replace('\r\n', '\n').replace('\r', '\n')
return text.strip()
def _is_table_line(self, line: str) -> bool:
"""Check if a line represents a table row"""
return bool(re.match(r'^\s*[^:]+:\s*[^:]+$', line) or
re.match(r'^\s*[^\t]+\t[^\t]+$', line))
def _extract_tables_from_text(self, content: str) -> Tuple[List[TableData], List[PlainText]]:
"""
Extract tables and plain text from content
Args:
content: Content to process
Returns:
Tuple of (list of tables, list of plain text sections)
"""
tables = []
plain_texts = []
# Process the entire content as plain text
plain_texts.append(PlainText(content=content, source_type='text_plain'))
return tables, plain_texts
def _anonymize_table(self, table: TableData) -> TableData:
"""Anonymize table data"""
try:
anonymized_table = TableData(
headers=table.headers.copy(),
rows=[row.copy() for row in table.rows],
source_type=table.source_type
)
for i, header in enumerate(anonymized_table.headers):
pattern = get_pattern_for_header(header, self.header_patterns)
if pattern:
for row in anonymized_table.rows:
if row[i] is not None:
original = str(row[i])
if original not in self.mapping:
self.mapping[original] = pattern.replacement_template.format(len(self.mapping) + 1)
row[i] = self.mapping[original]
return anonymized_table
except Exception as e:
logger.error(f"Error anonymizing table: {str(e)}")
logger.debug(traceback.format_exc())
raise
def _anonymize_plain_text(self, text: PlainText) -> PlainText:
"""Anonymize plain text content"""
try:
# Process the entire text at once instead of line by line
current_text = text.content
# Find all matches in the entire text
matches = find_patterns_in_text(current_text, self.data_patterns)
# Process matches in reverse order to avoid position shifting
for match in sorted(matches, key=lambda x: x[2], reverse=True):
pattern_name, matched_text, start, end = match
# Skip if the matched text is already a placeholder
if re.match(r'\[[A-Z_]+\d+\]', matched_text):
continue
# Find the pattern that matched
pattern = next((p for p in self.data_patterns if p.name == pattern_name), None)
if pattern:
# Use the pattern's replacement template
if matched_text not in self.mapping:
self.mapping[matched_text] = pattern.replacement_template.format(len(self.mapping) + 1)
replacement = self.mapping[matched_text]
# Replace the matched text while preserving surrounding whitespace
current_text = current_text[:start] + replacement + current_text[end:]
return PlainText(content=current_text, source_type=text.source_type)
except Exception as e:
logger.error(f"Error anonymizing plain text: {str(e)}")
logger.debug(traceback.format_exc())
raise
def _anonymize_json_value(self, value: Any, key: str = None) -> Any:
"""
Recursively anonymize JSON values based on their keys and content
Args:
value: Value to anonymize
key: Key name (if part of a key-value pair)
Returns:
Anonymized value
"""
if isinstance(value, dict):
return {k: self._anonymize_json_value(v, k) for k, v in value.items()}
elif isinstance(value, list):
return [self._anonymize_json_value(item) for item in value]
elif isinstance(value, str):
# Check if this is a key we should process
if key:
pattern = get_pattern_for_header(key, self.header_patterns)
if pattern:
if value not in self.mapping:
self.mapping[value] = pattern.replacement_template.format(len(self.mapping) + 1)
return self.mapping[value]
# Check if the value itself matches any patterns
matches = find_patterns_in_text(value, self.data_patterns)
if matches:
# Use the first match's pattern
pattern_name = matches[0][0]
if value not in self.mapping:
self.mapping[value] = f"{pattern_name.upper()}_{len(self.mapping) + 1}"
return self.mapping[value]
return value
else:
return value
def _anonymize_xml_element(self, element: ET.Element, indent: str = '') -> str:
"""
Recursively process XML element and return formatted string
Args:
element: XML element to process
indent: Current indentation level
Returns:
Formatted XML string
"""
# Process attributes
processed_attrs = {}
for attr_name, attr_value in element.attrib.items():
# Check if attribute name matches any header patterns
pattern = get_pattern_for_header(attr_name, self.header_patterns)
if pattern:
if attr_value not in self.mapping:
self.mapping[attr_value] = pattern.replacement_template.format(len(self.mapping) + 1)
processed_attrs[attr_name] = self.mapping[attr_value]
else:
# Check if attribute value matches any data patterns
matches = find_patterns_in_text(attr_value, self.data_patterns)
if matches:
pattern_name = matches[0][0]
pattern = next((p for p in self.data_patterns if p.name == pattern_name), None)
if pattern:
if attr_value not in self.mapping:
self.mapping[attr_value] = pattern.replacement_template.format(len(self.mapping) + 1)
processed_attrs[attr_name] = self.mapping[attr_value]
else:
processed_attrs[attr_name] = attr_value
else:
processed_attrs[attr_name] = attr_value
attrs = ' '.join(f'{k}="{v}"' for k, v in processed_attrs.items())
attrs = f' {attrs}' if attrs else ''
# Process text content
text = element.text.strip() if element.text and element.text.strip() else ''
if text:
# Check if text matches any patterns
matches = find_patterns_in_text(text, self.data_patterns)
if matches:
pattern_name = matches[0][0]
pattern = next((p for p in self.data_patterns if p.name == pattern_name), None)
if pattern:
if text not in self.mapping:
self.mapping[text] = pattern.replacement_template.format(len(self.mapping) + 1)
text = self.mapping[text]
# Process child elements
children = []
for child in element:
child_str = self._anonymize_xml_element(child, indent + ' ')
children.append(child_str)
# Build element string
if not children and not text:
return f"{indent}<{element.tag}{attrs}/>"
elif not children:
return f"{indent}<{element.tag}{attrs}>{text}</{element.tag}>"
else:
result = [f"{indent}<{element.tag}{attrs}>"]
if text:
result.append(f"{indent} {text}")
result.extend(children)
result.append(f"{indent}</{element.tag}>")
return '\n'.join(result)
def process_content(self, content: str, content_type: str) -> ProcessResult:
"""
Process content and return anonymized data
Args:
content: Content to process
content_type: Type of content ('csv', 'json', 'xml', 'text')
Returns:
ProcessResult: Contains anonymized data, mapping, replaced fields and processing info
"""
try:
replaced_fields = []
processed_info = {}
if content_type in ['csv', 'json', 'xml']:
# Handle as table
if content_type == 'csv':
df = pd.read_csv(StringIO(content), encoding='utf-8')
table = TableData(
headers=df.columns.tolist(),
rows=df.values.tolist(),
source_type='csv'
)
processed_info['type'] = 'table'
processed_info['headers'] = table.headers
processed_info['row_count'] = len(table.rows)
elif content_type == 'json':
data = json.loads(content)
# Process JSON recursively
result = self._anonymize_json_value(data)
processed_info['type'] = 'json'
return ProcessResult(result, self.mapping, replaced_fields, processed_info)
else: # xml
root = ET.fromstring(content)
# Process XML recursively with proper formatting
result = self._anonymize_xml_element(root)
processed_info['type'] = 'xml'
return ProcessResult(result, self.mapping, replaced_fields, processed_info)
if not table.rows:
return ProcessResult(None, self.mapping, [], processed_info)
anonymized_table = self._anonymize_table(table)
# Track replaced fields
for i, header in enumerate(anonymized_table.headers):
for orig_row, anon_row in zip(table.rows, anonymized_table.rows):
if anon_row[i] != orig_row[i]:
replaced_fields.append(header)
# Convert back to original format
if content_type == 'csv':
result = pd.DataFrame(anonymized_table.rows, columns=anonymized_table.headers)
elif content_type == 'json':
if len(anonymized_table.headers) == 1 and anonymized_table.headers[0] == 'value':
result = anonymized_table.rows[0][0]
else:
result = dict(zip(anonymized_table.headers, anonymized_table.rows[0]))
else: # xml
result = ET.tostring(root, encoding='unicode')
return ProcessResult(result, self.mapping, replaced_fields, processed_info)
else:
# Handle as text
# First, identify what needs to be replaced using table detection
tables, plain_texts = self._extract_tables_from_text(content)
processed_info['type'] = 'text'
processed_info['tables'] = [{'headers': t.headers, 'row_count': len(t.rows)} for t in tables]
# Process plain text sections
anonymized_texts = [self._anonymize_plain_text(text) for text in plain_texts]
# Combine all processed content
result = content
for text, anonymized_text in zip(plain_texts, anonymized_texts):
if text.content != anonymized_text.content:
result = result.replace(text.content, anonymized_text.content)
return ProcessResult(result, self.mapping, replaced_fields, processed_info)
except Exception as e:
logger.error(f"Error processing content: {str(e)}")
logger.debug(traceback.format_exc())
return ProcessResult(None, self.mapping, [], {'type': 'error', 'error': str(e)})

View file

@ -1,608 +0,0 @@
# Bewertung der PowerOn AI Platform
Basierend auf dem nachstehenden Q&A ergibt sich nachfolgende Bewertung des Softwareprodukts "PowerOn AI Platform".
## Aktueller Wert per Juni 2025
1. **Technischer Wert des Codes**:
- Professionelle, modulare Codebasis (~50.000-60.000 LOC)
- Moderne Architektur mit innovativen Komponenten
- 5 Personenmonate Entwicklung x CHF 15.000/PM = CHF 75.000
- Zusätzlicher Wert durch Enterprise-Ready Architektur: CHF 50.000
2. **Bisherige Investitionen**:
- Hardware/Software: CHF 20.000
- Expertise-Premium (30+ Jahre Erfahrung): CHF 25.000
3. **IP und Innovationswert**:
- Multi-Agent Workflow-System
- Modulare Architecture mit Alleinstellungsmerkmalen
- Geschätzter Wert: CHF 100.000
4. **Marktpotenzial-Faktor**:
- Adressierbares Marktvolumen von CHF 500-700 Mio.
- Wachstumsmarkt (25-30% jährlich)
- Frühphasen-Multiplikator: 2x
**Aktueller Gesamtwert (Juni 2025)**: **CHF 500.000**
## Prognostizierte Wertentwicklung
### Ende 2025
- Abschluss der technischen Entwicklung
- Erste Pilotprojekte mit 3-5 Referenzkunden
- Validierung des Produkts am Markt
- **Geschätzter Wert Ende 2025**: **CHF 1,2 Millionen**
(Steigerung durch Marktvalidierung und Risikoreduktion)
### Ende 2026
- 20-30 Kunden
- ARR: CHF 0,5-0,8 Mio.
- Etablierung im DACH-Markt
- **Geschätzter Wert Ende 2026**: **CHF 4-5 Millionen**
(Bewertungsmultiplikator von 6-8x ARR für wachstumsstarke SaaS)
### Ende 2027
- 70-90 Kunden
- ARR: CHF 2-2,5 Mio.
- Erweiterung der Produktpalette
- **Geschätzter Wert Ende 2027**: **CHF 12-15 Millionen**
(Bewertungsmultiplikator von 6x ARR)
### Ende 2028
- 150+ Kunden
- ARR: CHF 4,5 Mio.
- Internationale Expansion
- **Geschätzter Wert Ende 2028**: **CHF 25-30 Millionen**
(Bewertungsmultiplikator von 5,5-6,5x ARR für etablierte SaaS)
## Schlüsselfaktoren und Risikobeurteilung für die Wertentwicklung
1. **Erfolgreiche Markteinführung**: Der Übergang von Entwicklung zu erfolgreicher Pilotphase ist kritisch für die Wertentwicklung 2025-2026.
2. **Skalierung des Vertriebs**: Die Fähigkeit, die Kundenakquisition gemäss der Prognose zu skalieren, ist entscheidend für die 2026-[ADDRESS_2].
3. **Kapitaleffizienz**: Die effiziente Nutzung des Kapitals (CHF 750.000-1.050.000) für die nächste Entwicklungsphase wird die Bewertung massgeblich beeinflussen.
4. **Marktdynamik**: Die Entwicklung des KI-Marktes und regulatorische Änderungen können sowohl positive als auch negative Auswirkungen haben.
Diese Bewertung basiert auf der Annahme, dass die Meilensteine wie geplant erreicht werden und keine signifikanten externen Faktoren die Marktentwicklung negativ beeinflussen.
# Kriterienkatalog zur Softwarebewertung
## Teil 1: Technische Bewertung (Code-basiert)
1. Wie umfangreich ist die Codebasis (LOC, Module, Komponenten)?
2. Welche Programmiersprachen und Frameworks wurden verwendet?
3. Wie hoch ist die Codequalität und -konsistenz (saubere Architektur, Dokumentation, Tests)?
4. Gibt es innovative Algorithmen oder patentierbare technische Lösungen?
5. Wie modular und wartbar ist die Software gestaltet?
6. Wie robust ist die Fehlerbehandlung und Sicherheitsarchitektur?
7. Wie skalierbar ist die technische Infrastruktur?
8. Gibt es technische Schulden, die zukünftige Entwicklungen behindern könnten?
### 1. Umfang der Codebasis
- **Frontend**: Modulare JavaScript-Struktur mit ca. 15 Hauptmodulen
- **Backend**: Python/FastAPI mit ca. 15 Hauptmodulen
- **Hauptkomponenten**:
- Frontend: Workflow, UI, Koordination, Datenmanagement
- Backend: Gateway, Agent Service, Connectors, Workflow Manager
- **Geschätzte LOC**: ~50,000-60,000 Zeilen Code
### 2. Programmiersprachen und Frameworks
- **Frontend**:
- JavaScript (ES6+)
- Modulares System mit ES6-Import/Export
- Vanilla JS ohne externe Frameworks
- **Backend**:
- Python 3.x
- FastAPI für REST-API
- Asyncio für asynchrone Verarbeitung
### 3. Codequalität und -konsistenz
- **Saubere Architektur**:
- Klare Trennung Frontend/Backend
- Modulare Struktur mit definierten Verantwortlichkeiten
- State Machine Pattern für Workflow-Management
- **Dokumentation**:
- Ausführliche JSDoc/Python-Docstrings
- Architekturdiagramme (Mermaid)
- Technische Spezifikationen
- **Tests**:
- Automatisierte Modultests
- Manuell Integrationstests
- Benutzertests über Tickets in Clickup
### 4. Innovative Algorithmen/patentierbare Lösungen
- Multi-Agent Workflow-System mit spezialisierten Agenten
- Modulares Agent-Registry-System
- State Machine für Workflow-Koordination
- Dynamische Agenten-Integration
### 5. Modularität und Wartbarkeit
- **Hohe Modularität**:
- Klare Trennung der Verantwortlichkeiten
- Plug-and-Play Agent-System
- Erweiterbare Connector-Architektur
- **Wartbarkeit**:
- Konsistente Codestruktur
- Klare Namenskonventionen
- Dokumentierte Schnittstellen
### 6. Fehlerbehandlung und Sicherheit
- **Robuste Fehlerbehandlung**:
- State Machine für Workflow-Status
- Exception Handling auf allen Ebenen
- Logging-System für Debugging
- **Sicherheitsarchitektur**:
- Multi-Tenant-Architektur
- Authentifizierung/Autorisierung
- Mandantenverwaltung
### 7. Skalierbarkeit
- **Horizontale Skalierbarkeit**:
- Modulare Architektur
- Asynchrone Verarbeitung
- Connector-System für externe Dienste
- **Vertikale Skalierbarkeit**:
- Workflow-Parallelisierung
- Agent-Pooling
- Caching-Mechanismen
### 8. Technische Schulden
- **Potenzielle Verbesserungsbereiche**:
- Test-Coverage nicht sichtbar
- Eventuell fehlende Performance-Optimierungen
- Dokumentation für Zielgruppen noch unvollständig
- **Keine kritischen Blockierer identifiziert**
### Fazit der technischen Bewertung
Die Codebasis zeigt eine professionelle, gut strukturierte Enterprise-Anwendung mit klarer Architektur und modernen Best Practices. Die modulare Struktur und die saubere Implementierung der State Machine für Workflow-Management sind besonders hervorzuheben. Die Anwendung ist technisch reif und zeigt ein hohes Mass an Professionalität in der Implementierung.
## Teil 2: Bewertung der bisherigen Aufwände
### 1. Entwicklungsaufwände
- **Patrick**:
- ValueOn AG: 80 Stunden
- Private Entwicklung: 650 Stunden
- **Gesamt**: 730 Stunden (ca. 4.5 Personenmonate)
- **Ida**:
- ValueOn AG: 60 Stunden
- **Gesamt**: 60 Stunden (ca. 0.4 Personenmonate)
- **Gesamtaufwand**: ~5 Personenmonate
### 2. Qualifikationen und Erfahrungslevel
- **Patrick**:
- Business Consultant
- Software Architect
- Full Stack Developer
- 30+ Jahre Berufserfahrung
- Experte für Enterprise-Architekturen
- **Ida**:
- Business Analyst
- Project Manager
- Scrum Master
- Erfahrung in agiler Entwicklung
- Expertise in Prozessoptimierung
### 3. Spezifische Fachkenntnisse
- **Patrick**:
- Umfassende Markt- und Business-Erfahrung (30 Jahre)
- Expertise in Software-Architektur
- Full Stack Entwicklung
- Azure Cloud-Integration
- KI/ML Integration
- Enterprise-Systeme
- **Ida**:
- Projektmanagement
- Agile Methoden
- Business Analysis
- Prozessoptimierung
- Qualitätssicherung
### 4. Finanzielle Investitionen
- **Hardware**: CHF 10,000
- Entwicklungsserver
- Testumgebungen
- Entwicklungshardware
- **Software & Lizenzen**: CHF 10,000
- Entwicklungstools
- Cloud-Services
- KI-API-Zugänge
- **Gesamt**: CHF 20,000
### 5. Externe Dienstleister
- **Aktueller Status**: Keine externen Dienstleister
- **Vorteile**:
- Volle Kontrolle über Entwicklung
- Tiefes Verständnis der Architektur
- Schnelle Entscheidungswege
- Kosteneffizienz
### 6. Schlüsselkomponenten-Entwicklung
- **Patrick**:
- Frontend-Architektur
- Backend-System
- Workflow-Engine
- Agent-System
- Connector-Framework
- Datenmanagement
- Sicherheitsarchitektur
- **Verantwortlichkeiten**:
- Systemarchitektur
- Technische Leitung
- Code-Review
- Qualitätssicherung
### 7. Nicht-monetäre Ressourcen
- **Dominic**:
- Umfangreiches Sales & Marketing Netzwerk
- Marktzugang
- Branchenkontakte
- **ValueOn AG**:
- Infrastruktur
- Rechtlicher Rahmen
- Geschäftsprozesse
- **Netzwerke**:
- Technologie-Partner
- Potenzielle Kunden
- Branchenexperten
### 8. Finanzielle Risiken
- **ValueOn AG**:
- Bereitstellung von Infrastruktur
- Personelle Ressourcen
- Rechtlicher Rahmen
- **Private Investitionen**:
- Entwicklungszeit
- Hardware/Software
- Cloud-Services
### Fazit bisherige Aufwände
Die bisherigen Aufwände zeigen ein ausgewogenes Verhältnis zwischen technischer Expertise und Business-Know-how. Die private Investition von 730 Stunden durch Patrick demonstriert ein hohes Engagement und tiefes Verständnis der Technologie. Die Kombination aus technischer Expertise, Business-Erfahrung und Marktzugang bildet eine solide Grundlage für die weitere Entwicklung. Die bisherigen Investitionen sind effizient eingesetzt worden, mit Fokus auf kritische Kernkomponenten und skalierbare Architektur.
## Teil 3: Markt- und Geschäftspotenzial
### 1. Adressierbarer Gesamtmarkt und Wachstumspotenzial
- **Gesamtmarktvolumen 2025**:
- KI-Markt: $190 Mrd.
- Business Process Automation: $19,6 Mrd.
- Enterprise Knowledge Management: $43 Mrd.
- **Adressierbarer Markt (SAM)**:
- Initial: Mittlerer Markt in DACH (Professional Services, Finanzdienstleistungen, Gesundheitswesen)
- Geschätztes SAM: CHF 500-700 Mio.
- **Wachstumspotenzial**:
- Jährliches Marktwachstum: 25-30%
- Erweiterung auf internationale Märkte
- Branchenspezifische Lösungen
### 2. Alleinstellungsmerkmale
1. **Technologische Vorteile**:
- Proprietäre Multi-Agent-Technologie
- Modellunabhängige KI-Integration
- Enterprise-Ready Architektur
- Fortschrittliche Workflow-Orchestrierung
2. **Funktionale Vorteile**:
- Nahtlose Integration verschiedener KI-Modelle
- Robuste Fehlerbehandlung
- Skalierbare Multi-Tenant-Architektur
- Umfassende Enterprise-Features
### 3. Kunden und Pilotprojekte
- **Aktueller Status**:
- In Entwicklung
- Erste Referenzkunden in Planung
- Fokus auf mittelständische Unternehmen
- **Pilotphase**:
- Geplant für Q3/Q4 2025
- 3-5 Schlüsselreferenzkunden
- Branchenspezifische Templates
### 4. Geschäftsmodell
- **Hauptmodell**: SaaS (Software as a Service)
- **Preismodell**:
- Basis: Pro-Benutzer/Monat Abonnement
- Zusätzlich: Nutzungsbasierte Abrechnung
- Enterprise-Lizenzen für grössere Kunden
- **Erwartete Margen**: 75-85% nach Skalierung
### 5. Preisgestaltung
- **Wettbewerbsvergleich**:
- Unterhalb Enterprise-Lösungen
- Über Standard-BPA-Tools
- Flexiblere Preisgestaltung als Konkurrenz
- **Preisstruktur**:
- Basis-Abonnement: CHF 50-100 pro Benutzer/Monat
- Nutzungsbasierte Komponente: CHF 0.10-0.50 pro Verarbeitungseinheit
- Enterprise-Pakete: Individuelle Preisgestaltung
### 6. Umsatzpotenziale
- **Jahr 1 (2026)**:
- Ziel: 20-30 Kunden
- Erwartetes ARR: CHF 0.5-0.8 Mio.
- **Jahr 2 (2027)**:
- Ziel: 70-90 Kunden
- Erwartetes ARR: CHF 2-2.5 Mio.
- **Jahr 3 (2028)**:
- Ziel: 150+ Kunden
- Erwartetes ARR: CHF 4.5 Mio.
### 7. Akquisitionskosten
- **Customer Acquisition Cost (CAC)**:
- Erwarteter CAC: CHF 15,000-20,000
- Payback-Zeit: 12-18 Monate
- **Kostenstruktur**:
- 30% Vertrieb und Marketing
- Fokus auf effiziente Akquisition
- Skaleneffekte ab 50+ Kunden
### 8. Regulatorische Herausforderungen
- **Datenschutz**:
- DSGVO-Konformität
- Datensicherheit
- Mandantentrennung
- **KI-Regulierung**:
- EU AI Act
- Transparenzpflichten
- Qualitätssicherung
- **Branchenspezifische Regulierung**:
- Finanzdienstleistungen
- Gesundheitswesen
- Professional Services
### Fazit Markt- und Geschäftspotenzial
Die PowerOn AI Platform adressiert einen wachsenden Markt mit klaren Alleinstellungsmerkmalen. Das Geschäftsmodell ist skalierbar und die Preisgestaltung wettbewerbsfähig. Die regulatorischen Herausforderungen sind bekannt und adressierbar. Die Umsatzprognosen sind konservativ kalkuliert und basieren auf realistischen Marktannahmen.
## Teil 4: Skalierungs- und Zukunftspotenzial
### 1. Ressourcen für Support und Weiterentwicklung
- **Entwicklungsteam**:
- 2-3 Full-Stack Entwickler, KI-unterstützte Entwicklung
- 1 DevOps Engineer
- 1 QA Engineer
- **Support-Team**:
- 1-2 Support Engineers
- 1 Technical Account Manager
- **Infrastruktur**:
- Cloud-basierte Skalierung (Azure)
- Automatisierte Deployment-Pipeline
- Monitoring und Logging-Systeme
### 2. Skalierbarkeit
- **Technische Skalierbarkeit**:
- Horizontale Skalierung durch Multi-Tenant-Architektur
- Vertikale Skalierung durch Agent-Pooling
- Automatische Lastverteilung
- **Skalierungszeitrahmen**:
- 2x Nutzer: Sofort möglich
- 5x Nutzer: 1-2 Monate Vorbereitung
- 10x Nutzer: 3-4 Monate mit Infrastruktur-Erweiterung
### 3. Kapitalbedarf
- **Nächste Entwicklungsphase (12 Monate)**:
- Entwicklung: CHF 400,000-500,000
- Marketing & Sales: CHF 100,000-200,000
- Infrastruktur: CHF 50,000-100,000
- Betrieb & Support: CHF 100,000-150,000
- **Gesamt**: CHF 750,000-1,050,000
### 4. Schlüsselpersonen
- **Technische Leitung**:
- Patrick (Software Architect, Full Stack Developer)
- Verantwortlich für: Architektur, Entwicklung, Technische Strategie
- **Business & Operations**:
- Ida (Business Analyst, Project Manager)
- Verantwortlich für: Projektmanagement, Business Analysis
- **Sales & Marketing**:
- Dominic (Sales & Marketing)
- Verantwortlich für: Marktentwicklung, Kundenakquisition
### 5. Exit-Strategien
- **Strategische Übernahme**:
- Enterprise Software Anbieter
- KI/ML Plattform Betreiber
- Business Process Automation Unternehmen
- **IPO-Potenzial**:
- Ab CHF 50 Mio. ARR
- Zeitrahmen: 5-7 Jahre
- **Extraktion aus ValueOn AG**:
- Vergütung der Aufwände
- Anrechnung des Mehrwerts für Schlüsselpersonen
- Beschaffung des notwendigen Kapitals
### 6. Strategische Partnerschaften
- **Technologie-Partner**:
- KI-Provider (OpenAI, Anthropic)
- Cloud-Provider (Azure)
- Enterprise Software Anbieter
- **Vertriebspartner**:
- Systemhäuser
- Beratungsunternehmen
- Branchenspezialisten
- **Forschungspartner**:
- Universitäten
- Forschungsinstitute
- KI-Labore
### 7. Geplante Erweiterungen
- **Kurzfristig (12 Monate)**:
- Erweiterte Agent-Typen
- Branchenspezifische Templates
- API-Erweiterungen
- **Mittelfristig (24 Monate)**:
- Agentenmarktplatz
- Proprietäre KI-Modelle
- Erweiterte Analytics
- **Langfristig (36+ Monate)**:
- KI-Middleware für Unternehmen
- Branchenlösungen
- Internationale Expansion
### 8. Langfristige Vision
- **Technologische Vision**:
- Führende Multi-Agent KI-Plattform
- Standard für Enterprise Workflow Automation
- Innovationstreiber in der KI-Integration
- **Marktvision**:
- Globaler Marktführer in Nischenbereichen
- Branchenstandard für bestimmte Anwendungsfälle
- Referenz für KI-gestützte Prozessoptimierung
- **Geschäftsvision**:
- Nachhaltiges Wachstum
- Profitables Geschäftsmodell
- Führende Position in ausgewählten Märkten
### Fazit Skalierungs- und Zukunftspotenzial
Die PowerOn AI Platform verfügt über ein solides Skalierungspotenzial sowohl technisch als auch geschäftlich. Die modulare Architektur ermöglicht schnelles Wachstum, während die klare Vision und die strategischen Partnerschaften den langfristigen Erfolg unterstützen. Die Kapitalanforderungen sind realistisch kalkuliert und die Exit-Strategien bieten verschiedene Optionen für die Zukunft.
# Exitplan: PowerOn AI Platform als eigenständige AG
## Bewertung und Ausgangssituation
**Aktueller Wert (Juni 2025)**: CHF 500.000
**Angepasster Gründungswert**: CHF 800.000 (berücksichtigt den strategischen Wert der Produktvision, welche bereits als innerer Wert im Produkt enthalten ist)
## Strukturierung der PowerOn AG
### 1. Aktienstruktur bei Gründung
**Gesamtes Aktienkapital**: 1.000.000 Aktien (Nennwert CHF 0,10)
**Firmenvaluation bei Gründung**: CHF 800.000
#### Verteilung der initialen Aktien:
- **Patrick**:
- Eingebrachte Leistung: Entwicklung, technische Expertise und essenzielles Gesamtkonzept
- **Aktienanteil**: 35% (350.000 Aktien)
- **Dominic**:
- Eingebrachte Leistung: Netzwerk, Sales & Marketing Expertise und Vision
- **Aktienanteil**: 15% (150.000 Aktien)
- **ValueOn AG**:
- Eingebrachte Leistung: Infrastruktur, rechtlicher Rahmen, Arbeitszeit, Übertragung von IP
- **Aktienanteil**: 25% (250.000 Aktien)
- **Reservierter Anteil für Mitarbeiter-Pool**:
- **Aktienanteil**: 10% (100.000 Aktien)
- **Reserviert für Investoren (erste Runde)**:
- **Aktienanteil**: 15% (150.000 Aktien)
### 2. Kapitalbedarfsplanung (18 Monate)
| Kategorie | Betrag (CHF) |
|-----------|--------------|
| Entwicklung | 550.000 |
| Marketing & Sales | 250.000 |
| Infrastruktur | 100.000 |
| Betrieb & Support | 200.000 |
| **Gesamtbedarf** | **1.100.000** |
**Kapitalbeschaffungsstrategie**:
- **Erste Finanzierungsrunde**: CHF 1.000.000 (für 18 Monate)
- **Sicherheitspuffer**: CHF 100.000 (aus Umsätzen/zukünftigen Einnahmen)
### 3. Investitionskonditionen
**Pre-Money Bewertung**: CHF 800.000
**Investitionsvolumen**: CHF 1.000.000
**Post-Money Bewertung**: CHF 1.800.000
**Aktienkurs für Investoren**:
- 150.000 bestehende Aktien + 214.285 neue Aktien = 364.285 Aktien für Investoren
- **Aktienkurs**: CHF 2,75 pro Aktie
**Aktienstruktur nach Investment**:
- Patrick: 35% → 28,9% (350.000 Aktien)
- Dominic: 15% → 12,4% (150.000 Aktien)
- ValueOn AG: 25% → 20,7% (250.000 Aktien)
- Mitarbeiter-Pool: 10% → 8,3% (100.000 Aktien)
- Investoren: 29,7% (364.285 Aktien)
## Governance und Organisation
### 1. Schlüsselpositionen in der Organisation
- **CEO**: Gesamtführung des Unternehmens
- **CTO**: Verantwortlich für technische Strategie und Produktentwicklung
- **CSO/Vertriebsleitung**: Verantwortlich für Vertrieb und Marktentwicklung
- **COO**: Operative Leitung und Geschäftsprozesse
Die Besetzung dieser Positionen wird unter Berücksichtigung der Kompetenzen von Patrick, Dominic und möglichen neuen Führungskräften festgelegt. Die Rollen von CEO und COO werden im Rahmen der Unternehmensgründung evaluiert.
### 2. Vergütungsstruktur
- **Führungsebene**: Marktübliche Vergütung zwischen CHF 150.[PHONE_1]ahr je nach Position
- **Aktienoptionen**: Zusätzliche Aktienoptionen bei Erreichen definierter Unternehmensziele
## Meilensteine und Finanzielle Ziele
### Kritische Meilensteine (18 Monate)
| Zeitpunkt | Meilenstein | KPI |
|-----------|-------------|-----|
| Q3 2025 | Ausgründung & Finanzierung | Abschluss der Seed-Runde |
| Q4 2025 | Markteinführung | 3-5 Pilotprojekte |
| Q1 2026 | Produktvalidierung | 10+ zahlende Kunden |
| Q2 2026 | Skalierung | 15+ zahlende Kunden |
| Q4 2026 | Vorbereitung Serie A | 25+ Kunden, ARR: CHF 0,8 Mio. |
### Umsatz- und Bewertungsprognose
| Jahr | Kunden | ARR (CHF) | Valuation (CHF) | Multiplikator |
|------|--------|-----------|-----------------|---------------|
| Ende 2025 | 5-8 | 0,2 Mio. | 1,8 Mio. | 9x ARR |
| Ende 2026 | 25-30 | 0,8 Mio. | 5,6 Mio. | 7x ARR |
| Ende 2027 | 80-90 | 2,5 Mio. | 15 Mio. | 6x ARR |
| Ende 2028 | 150+ | 4,5 Mio. | 27 Mio. | 6x ARR |
## Liquiditätsoptionen
### Mittelfristige Optionen (2-3 Jahre)
1. **Serie A Finanzierung** (Ende 2026):
- Teilweise Liquidität für Gründer und ValueOn AG (10-15% ihrer Anteile)
- Zu erwartender Wert: CHF 5-6 Mio.
2. **Strategische Partnerschaft**:
- Investment durch strategischen Partner mit teilweisem Aktienrückkauf
- Potenzielle Partner: Enterprise Software-Anbieter, KI-Plattform-Betreiber
### Langfristige Optionen (4-7 Jahre)
1. **Komplette Übernahme**:
- Erwarteter Exit-Wert 2028: CHF 25-30 Mio.
- Vollständige Liquidität für alle Anteilseigner
2. **IPO-Vorbereitung**:
- Bei Erreichen von CHF 10+ Mio. ARR
- Potenzielle Bewertung: CHF 50-70 Mio.
## Nächste Schritte im Ausgründungsprozess
1. **Rechtliche Strukturierung**:
- Gründung der PowerOn AG
- Übertragungsvereinbarungen für geistiges Eigentum
- Aktionärsvereinbarungen
2. **Finanzierung**:
- Erstellung eines detaillierten Businessplans
- Vorbereitung des Investor Pitch Decks
- Ansprache potenzieller Investoren
3. **Organisationsaufbau**:
- Definition der Führungsstruktur und Schlüsselpositionen
- Rekrutierung des Kernteams
- Aufbau der operativen Prozesse
4. **Markteinführungsstrategie**:
- Festlegung der Go-to-Market Strategie
- Identifikation von Pilotprojekten
- Vorbereitung der Vertriebsunterlagen

View file

@ -1,6 +0,0 @@
id,name,email,phone,address,iban,credit_card,ahv_number
1,[NAME_3],[EMAIL_8],[PHONE_13],[ADDRESS_18],[IBAN_23],[IBAN_28],[SSN_33]
2,[NAME_4],[EMAIL_9],[PHONE_14],[ADDRESS_19],[IBAN_24],[IBAN_29],[SSN_34]
3,[NAME_5],[EMAIL_10],[PHONE_15],[ADDRESS_20],[IBAN_25],[IBAN_30],[SSN_33]
4,[NAME_6],[EMAIL_11],[PHONE_16],[ADDRESS_21],[IBAN_26],[IBAN_31],[SSN_33]
5,[NAME_7],[EMAIL_12],[PHONE_17],[ADDRESS_22],[IBAN_27],[IBAN_32],[SSN_35]
1 id name email phone address iban credit_card ahv_number
2 1 [NAME_3] [EMAIL_8] [PHONE_13] [ADDRESS_18] [IBAN_23] [IBAN_28] [SSN_33]
3 2 [NAME_4] [EMAIL_9] [PHONE_14] [ADDRESS_19] [IBAN_24] [IBAN_29] [SSN_34]
4 3 [NAME_5] [EMAIL_10] [PHONE_15] [ADDRESS_20] [IBAN_25] [IBAN_30] [SSN_33]
5 4 [NAME_6] [EMAIL_11] [PHONE_16] [ADDRESS_21] [IBAN_26] [IBAN_31] [SSN_33]
6 5 [NAME_7] [EMAIL_12] [PHONE_17] [ADDRESS_22] [IBAN_27] [IBAN_32] [SSN_35]

View file

@ -1,69 +0,0 @@
Lebenslauf: Lara Meier
Persönliche Daten:
Name: Lara Meier
Geboren: 15.03.[ADDRESS_55]: [ADDRESS_54], [ADDRESS_53]
Telefon: [PHONE_40] 67
E-Mail: [EMAIL_52]
AHV-Nr.: [SSN_34]
Steuernummer: [SSN_48]
Berufserfahrung:
2020-2023: Senior Projektmanagerin
Firma: TechSolutions AG
Adresse: [ADDRESS_51], [ADDRESS_50]
UID: [SSN_49]lefon: [PHONE_37] 43
E-Mail: [EMAIL_47]
In dieser Position leitete ich ein Team von 15 Mitarbeitern und verantwortete die Implementierung von Cloud-Lösungen für internationale Kunden. Meine Hauptaufgaben umfassten:
- Projektplanung und -steuerung mit einem Budget von CHF 2.5 Mio.
- Kundenbetreuung und Stakeholder-Management
- Teamführung und Personalentwicklung
- Qualitätssicherung und Risikomanagement
2015-2020: Projektmanagerin
Firma: Digital Systems GmbH
Adresse: [ADDRESS_46], [ADDRESS_45]
UID: [SSN_44]lefon: [PHONE_42] 90
E-Mail: [EMAIL_41]
Als Projektmanagerin verantwortete ich die erfolgreiche Durchführung von Digitalisierungsprojekten. Meine Leistungen:
- Implementierung von ERP-Systemen
- Optimierung von Geschäftsprozessen
- Schulung von Endbenutzern
- Erstellung von Projektdokumentation
Ausbildung:
2010-2015: ETH Zürich
Studiengang: Informatik
Matrikelnummer: 12-345-678
Abschluss: Master of Science in Computer Science
Thesis: "Künstliche Intelligenz in der Prozessautomatisierung"
2005-2010: Kantonsschule Zürich
Abschluss: Eidgenössische Maturität
Schwerpunkt: Mathematik und Naturwissenschaften
Sprachen:
Deutsch (Muttersprache)
Englisch (C2)
Französisch (B2)
Italienisch (B1)
Zertifizierungen:
PMP (Project Management Professional)
ITIL v4 Foundation
AWS Certified Solutions Architect
Scrum Master (PSM I)
Referenzen:
Prof. Dr. Hans Müller
ETH Zürich
Department of Computer Science
Telefon: [PHONE_40] 68
E-Mail: [EMAIL_39]
[NAME_38] AG
CTO
Telefon: [PHONE_37] 44
E-Mail: [EMAIL_36]

View file

@ -1,20 +0,0 @@
employee_id,first_name,last_name,email,phone,department,office_address,uid_number,bank_account
E001,[NAME_56],[NAME_61],[EMAIL_66],[PHONE_71],IT,[ADDRESS_76],,[IBAN_23]
E002,[NAME_57],[NAME_62],[EMAIL_67],[PHONE_72],HR,[ADDRESS_77],CHE-123.456.789,[IBAN_81]
E003,[NAME_58],[NAME_63],[EMAIL_68],[PHONE_73],Finance,[ADDRESS_78],,[IBAN_25]
E004,[NAME_59],[NAME_64],[EMAIL_69],[PHONE_74],Marketing,[ADDRESS_79],,[IBAN_26]
E005,[NAME_60],[NAME_65],[EMAIL_70],[PHONE_75],Sales,[ADDRESS_80],,[IBAN_82]
[REINTEGRATE]
The employee database has 9 attributes,
uid_number and bank_account is not defined for all records.
1 employee_id,first_name,last_name,email,phone,department,office_address,uid_number,bank_account
2 E001,[NAME_56],[NAME_61],[EMAIL_66],[PHONE_71],IT,[ADDRESS_76],,[IBAN_23]
3 E002,[NAME_57],[NAME_62],[EMAIL_67],[PHONE_72],HR,[ADDRESS_77],CHE-123.456.789,[IBAN_81]
4 E003,[NAME_58],[NAME_63],[EMAIL_68],[PHONE_73],Finance,[ADDRESS_78],,[IBAN_25]
5 E004,[NAME_59],[NAME_64],[EMAIL_69],[PHONE_74],Marketing,[ADDRESS_79],,[IBAN_26]
6 E005,[NAME_60],[NAME_65],[EMAIL_70],[PHONE_75],Sales,[ADDRESS_80],,[IBAN_82]
7 [REINTEGRATE]
8 The employee database has 9 attributes,
9 uid_number and bank_account is not defined for all records.

View file

@ -1,9 +0,0 @@
Dear Mr. Smith,
Thank you for your email to [EMAIL_85].
Your contact details have been recorded:
Phone: [PHONE_17]RESS_84]: 123 High Street, London SW1A 1AA
Your IBAN: GB29 NWBK 6016 1331 9268 19
Credit Card: 4532 1234 5678 [ADDRESS_83] regards,
John Doe

View file

@ -1,54 +0,0 @@
{
"customer": {
"personal_info": {
"name": "[NAME_3]",
"email": "[EMAIL_86]",
"phone": "[PHONE_87]",
"address": {
"street": "Hauptstra\u00dfe 123",
"city": "M\u00fcnchen",
"zip": "80331",
"country": "Deutschland"
}
},
"order_history": [
{
"order_id": "ORD-2024-001",
"date": "[DATE_88]",
"items": [
{
"product": "Laptop",
"price": 1299.99,
"payment": {
"method": "credit_card",
"card_number": "[IBAN_89]",
"iban": "[IBAN_23]"
}
}
]
}
],
"preferences": {
"language": "de",
"newsletter": true,
"marketing_consent": {
"email": "[EMAIL_86]",
"phone": "[PHONE_87]"
}
}
},
"company": {
"name": "[NAME_90]",
"contact": {
"manager": "NAME_91",
"email": "[EMAIL_92]",
"phone": "[PHONE_71]",
"address": {
"street": "Technologiestra\u00dfe 45",
"city": "Berlin",
"zip": "10115",
"country": "Deutschland"
}
}
}
}

View file

@ -1,78 +0,0 @@
<company_data>
<employees>
<employee id="EMP001">
<personal_info>
<name>[NAME_93]</name>
<email>[EMAIL_94]</email>
<phone>[PHONE_13]</phone>
<address>
<street>Musterstraße 42</street>
<city>Hamburg</city>
<zip>20095</zip>
<country>Deutschland</country>
</address>
</personal_info>
<employment>
<department>IT</department>
<position>Senior Developer</position>
<salary currency="EUR">75000</salary>
<bank_details>
<iban>[IBAN_23]</iban>
<bic>COBADEFFXXX</bic>
</bank_details>
</employment>
</employee>
<employee id="EMP002">
<personal_info>
<name>Maria Schmidt</name>
<email>[EMAIL_95]</email>
<phone>[PHONE_96]</phone>
<address>
<street>Bahnhofstraße 15</street>
<city>Frankfurt</city>
<zip>60329</zip>
<country>Deutschland</country>
</address>
</personal_info>
<employment>
<department>HR</department>
<position>HR Manager</position>
<salary currency="EUR">65000</salary>
<bank_details>
<iban>[PHONE_97]</iban>
<bic>COBADEFFXXX</bic>
</bank_details>
</employment>
</employee>
</employees>
<projects>
<project id="PRJ001">
<name>Digital Transformation</name>
<manager>[NAME_93]</manager>
<budget currency="EUR">150000</budget>
<team_members>
<member email="[EMAIL_94]">[NAME_93]</member>
<member email="[EMAIL_95]">Maria Schmidt</member>
</team_members>
</project>
</projects>
<company_info>
<name>Tech Innovations GmbH</name>
<address>
<street>Innovationsstraße 1</street>
<city>München</city>
<zip>80331</zip>
<country>Deutschland</country>
</address>
<contact>
<email>[EMAIL_98]</email>
<phone>[PHONE_71]</phone>
<fax>[PHONE_99]</fax>
</contact>
<banking>
<iban>[PHONE_100]</iban>
<bic>COBADEFFXXX</bic>
<tax_id>DE123456789</tax_id>
</banking>
</company_info>
</company_data>

View file

@ -1,10 +0,0 @@
Bonjour Madame Dupont,
Merci pour votre email à [EMAIL_104].
Vos coordonnées ont été enregistrées:
Téléphone: [PHONE_103] 67 89
Adresse: 123 Rue de Paris, 75001 Paris
Votre IBAN: FR76 3000 6000 [PHONE_102]9
Carte de crédit: 4532 1234 5678 [ADDRESS_101],
Jean Martin

View file

@ -1,24 +0,0 @@
Sehr geehrte Frau [NAME_12],
vielen Dank für Ihre E-Mail an [EMAIL_8].
Ihre Telefonnummer [PHONE_108]urde in unserem System hinterlegt.
Die Rechnung wird an folgende Adresse gesendet:
Musterstraße 123, 12345 Berlin
Ihre IBAN: DE89 3704 [PHONE_107]Kreditkarte: 4532 1234 5678 [ADDRESS_105] freundlichen Grüßen
Max Mustermann
[REINTEGRATE]
Brief von Frau Müller zur
Rechnungsnachfrage an den Helpdesk

View file

@ -1,54 +0,0 @@
Geschäftsstrategie 2024-[ADDRESS_112] AG
Unternehmensdaten:
Firmenname: TechSolutions AG
UID: [SSN_49]resse: [ADDRESS_51], [ADDRESS_50]
Telefon: [PHONE_37] 43
E-Mail: [EMAIL_111]
Website: www.techsolutions.ch
Geschäftsführung:
CEO: [NAME_93]
E-Mail: [EMAIL_110]
Telefon: [PHONE_37] 44
CTO: Dr. Sarah Müller
E-Mail: [EMAIL_109]
Telefon: [PHONE_37] 45
Finanzdaten:
Bank: UBS AG
IBAN: CH93 0076 7000 E529 0767 9
BIC: UBSWCHZH80A
Strategische Ziele:
1. Marktpositionierung
- Marktanteil in der Schweiz auf 25% steigern
- Expansion in DACH-Region
- Neue Zielgruppe: KMU
2. Produktentwicklung
- Cloud-Lösung für KMU
- KI-basierte Automatisierung
- Mobile App für Kunden
3. Personalentwicklung
- Team auf 50 Mitarbeiter ausbauen
- Neue Standorte: Bern und Genf
- Fokus auf Diversity & Inclusion
4. Nachhaltigkeit
- CO2-Neutralität bis 2025
- Green IT Zertifizierung
- Nachhaltige Lieferkette
Budgetplanung:
2024: CHF 5.2 Mio.
2025: CHF 6.8 Mio.
2026: CHF 8.5 Mio.
Risikoanalyse:
- Marktrisiken
- Technologische Risiken
- Personelle Risiken
- Finanzielle Risiken

View file

@ -1,6 +0,0 @@
geschäft_id,datum,kundenname,kundenemail,betrag,zahlungsmethode,zahlungsdetails,lieferadresse
2024-03-15,[DATE_113],[NAME_118],[EMAIL_123],50,IBAN,[IBAN_23],[ADDRESS_132]
2024-03-15,[DATE_114],[NAME_119],[EMAIL_124],75,IBAN,[IBAN_128],[ADDRESS_133]
2024-03-15,[DATE_115],[NAME_120],[EMAIL_125],25,IBAN,[IBAN_129],[ADDRESS_134]
2024-03-15,[DATE_116],[NAME_121],[EMAIL_126],0,IBAN,[IBAN_130],[ADDRESS_135]
2024-03-15,[DATE_117],[NAME_122],[EMAIL_127],50,IBAN,[IBAN_131],[ADDRESS_136]
1 geschäft_id datum kundenname kundenemail betrag zahlungsmethode zahlungsdetails lieferadresse
2 2024-03-15 [DATE_113] [NAME_118] [EMAIL_123] 50 IBAN [IBAN_23] [ADDRESS_132]
3 2024-03-15 [DATE_114] [NAME_119] [EMAIL_124] 75 IBAN [IBAN_128] [ADDRESS_133]
4 2024-03-15 [DATE_115] [NAME_120] [EMAIL_125] 25 IBAN [IBAN_129] [ADDRESS_134]
5 2024-03-15 [DATE_116] [NAME_121] [EMAIL_126] 0 IBAN [IBAN_130] [ADDRESS_135]
6 2024-03-15 [DATE_117] [NAME_122] [EMAIL_127] 50 IBAN [IBAN_131] [ADDRESS_136]

View file

@ -1,9 +0,0 @@
Buongiorno Signora Rossi,
Grazie per la sua email a [EMAIL_141].
Grazie per la sua email a [EMAIL_141] .
I suoi dati di contatto sono stati registrati:
Telefono: [PHONE_16]S_139]: Via Roma 123, 20100 Milano
Il suo IBAN: IT60 X054 2811 1010 [PHONE_138]arta di credito: 4532 1234 5678 [ADDRESS_137] saluti,
Marco Bianchi

View file

@ -1,6 +0,0 @@
kunden_id,vorname,nachname,email,telefon,adresse,iban,kreditkarte,steuernummer
K001,[NAME_142],[NAME_146],[EMAIL_149],[PHONE_13],[ADDRESS_18],[IBAN_23],[IBAN_28],[SSN_162]
K002,[NAME_143],[NAME_62],[EMAIL_150],[PHONE_154],[ADDRESS_158],[IBAN_128],[IBAN_29],[SSN_163]
K003,[NAME_57],[NAME_61],[EMAIL_151],[PHONE_155],[ADDRESS_159],[IBAN_129],[IBAN_30],[SSN_164]
K004,[NAME_144],[NAME_147],[EMAIL_152],[PHONE_156],[ADDRESS_160],[IBAN_130],[IBAN_31],[SSN_165]
K005,[NAME_145],[NAME_148],[EMAIL_153],[PHONE_157],[ADDRESS_161],[IBAN_131],[IBAN_32],[SSN_166]
1 kunden_id vorname nachname email telefon adresse iban kreditkarte steuernummer
2 K001 [NAME_142] [NAME_146] [EMAIL_149] [PHONE_13] [ADDRESS_18] [IBAN_23] [IBAN_28] [SSN_162]
3 K002 [NAME_143] [NAME_62] [EMAIL_150] [PHONE_154] [ADDRESS_158] [IBAN_128] [IBAN_29] [SSN_163]
4 K003 [NAME_57] [NAME_61] [EMAIL_151] [PHONE_155] [ADDRESS_159] [IBAN_129] [IBAN_30] [SSN_164]
5 K004 [NAME_144] [NAME_147] [EMAIL_152] [PHONE_156] [ADDRESS_160] [IBAN_130] [IBAN_31] [SSN_165]
6 K005 [NAME_145] [NAME_148] [EMAIL_153] [PHONE_157] [ADDRESS_161] [IBAN_131] [IBAN_32] [SSN_166]

View file

@ -1,6 +0,0 @@
mitarbeiter_id,vorname,nachname,email,telefon,abteilung,büroadresse,steuernummer,sozialversicherungsnummer
M001,[NAME_167],[NAME_172],[EMAIL_177],[PHONE_182],IT,[ADDRESS_187],[SSN_162],[SSN_192]
M002,[NAME_168],[NAME_173],[EMAIL_178],[PHONE_183],HR,[ADDRESS_188],[SSN_163],[SSN_193]
M003,[NAME_169],[NAME_174],[EMAIL_179],[PHONE_184],Finanzen,[ADDRESS_189],[SSN_164],[SSN_194]
M004,[NAME_170],[NAME_175],[EMAIL_180],[PHONE_185],Marketing,[ADDRESS_190],[SSN_165],[SSN_195]
M005,[NAME_171],[NAME_176],[EMAIL_181],[PHONE_186],Vertrieb,[ADDRESS_161],[SSN_191],[SSN_196]
1 mitarbeiter_id vorname nachname email telefon abteilung büroadresse steuernummer sozialversicherungsnummer
2 M001 [NAME_167] [NAME_172] [EMAIL_177] [PHONE_182] IT [ADDRESS_187] [SSN_162] [SSN_192]
3 M002 [NAME_168] [NAME_173] [EMAIL_178] [PHONE_183] HR [ADDRESS_188] [SSN_163] [SSN_193]
4 M003 [NAME_169] [NAME_174] [EMAIL_179] [PHONE_184] Finanzen [ADDRESS_189] [SSN_164] [SSN_194]
5 M004 [NAME_170] [NAME_175] [EMAIL_180] [PHONE_185] Marketing [ADDRESS_190] [SSN_165] [SSN_195]
6 M005 [NAME_171] [NAME_176] [EMAIL_181] [PHONE_186] Vertrieb [ADDRESS_161] [SSN_191] [SSN_196]

View file

@ -1,10 +0,0 @@
Guten Tag [NAME_200] für Ihre Anfrage an [EMAIL_199].
Ihre Kontaktdaten wurden wie folgt erfasst:
Telefon: [PHONE_40] 67
Adresse: [ADDRESS_198], [ADDRESS_53]
Ihre AHV-Nummer: [SSN_34]
UID-Nummer: [SSN_49]nkkonto: [PHONE_197]7]-8
Mit freundlichen Grüßen
Peter Muster

View file

@ -1,6 +0,0 @@
transaction_id,date,customer_name,customer_email,amount,payment_method,payment_details,shipping_address
T001,[DATE_88],[DATE_113],[NAME_118],1250.5,IBAN,[IBAN_23],[ADDRESS_132]
T002,[DATE_88],[DATE_114],[EMAIL_201],890.75,AHV,[SSN_34],[ADDRESS_205]
T003,[DATE_88],[DATE_115],[EMAIL_202],2340.25,IBAN,[IBAN_25],[ADDRESS_206]
T004,[DATE_88],[DATE_116],[EMAIL_203],1750.0,IBAN,[IBAN_26],[ADDRESS_207]
T005,[DATE_88],[DATE_117],[EMAIL_204],3200.5,IBAN,[IBAN_27],[ADDRESS_208]
1 transaction_id date customer_name customer_email amount payment_method payment_details shipping_address
2 T001 [DATE_88] [DATE_113] [NAME_118] 1250.5 IBAN [IBAN_23] [ADDRESS_132]
3 T002 [DATE_88] [DATE_114] [EMAIL_201] 890.75 AHV [SSN_34] [ADDRESS_205]
4 T003 [DATE_88] [DATE_115] [EMAIL_202] 2340.25 IBAN [IBAN_25] [ADDRESS_206]
5 T004 [DATE_88] [DATE_116] [EMAIL_203] 1750.0 IBAN [IBAN_26] [ADDRESS_207]
6 T005 [DATE_88] [DATE_117] [EMAIL_204] 3200.5 IBAN [IBAN_27] [ADDRESS_208]

View file

@ -1,396 +0,0 @@
"""
Pattern definitions for data anonymization
Separates header patterns from data patterns
"""
from dataclasses import dataclass
from typing import List, Optional, Tuple
import re
@dataclass
class Pattern:
"""Base class for patterns"""
name: str
patterns: List[str]
replacement_template: str
class HeaderPatterns:
"""Patterns for identifying sensitive data in headers"""
patterns = [
# Name patterns
Pattern(
name="name",
patterns=[
# Simple variations
r'\b(?:name|first[-_\s]*name|last[-_\s]*name|full[-_\s]*name)\b',
r'\b(?:customer[-_\s]*name|client[-_\s]*name|user[-_\s]*name)\b',
r'\b(?:given[-_\s]*name|family[-_\s]*name|surname)\b',
# German variations
r'\b(?:vorname|nachname|vollständiger[-_\s]*name|name)\b',
r'\b(?:kunden[-_\s]*name|kunde[-_\s]*name|benutzer[-_\s]*name)\b',
# French variations
r'\b(?:prénom|nom|nom[-_\s]*complet)\b',
r'\b(?:nom[-_\s]*du[-_\s]*client|nom[-_\s]*d\'utilisateur)\b',
# Italian variations
r'\b(?:nome|cognome|nome[-_\s]*completo)\b',
r'\b(?:nome[-_\s]*cliente|nome[-_\s]*utente)\b',
# Common variations
r'\b(?:nom|name|nome|naam)\b'
],
replacement_template="[NAME_{}]"
),
# Email patterns
Pattern(
name="email",
patterns=[
# Simple variations - only labels
r'\b(?:email|e[-_\s]*mail|mail)\s*:?\b',
r'\b(?:contact[-_\s]*email|user[-_\s]*email|client[-_\s]*email)\s*:?\b',
r'\b(?:customer[-_\s]*email|customer[-_\s]*mail|customer[-_\s]*e[-_\s]*mail)\s*:?\b',
# German variations - only labels
r'\b(?:e[-_\s]*mail|e[-_\s]*post|mail[-_\s]*adresse)\s*:?\b',
r'\b(?:kontakt[-_\s]*email|benutzer[-_\s]*email|kunden[-_\s]*email)\s*:?\b',
r'\b(?:kunden[-_\s]*mail|kunden[-_\s]*e[-_\s]*mail|kunden[-_\s]*e[-_\s]*post)\s*:?\b',
# French variations - only labels
r'\b(?:courriel|e[-_\s]*mail|adresse[-_\s]*e[-_\s]*mail)\s*:?\b',
r'\b(?:courriel[-_\s]*de[-_\s]*contact|e[-_\s]*mail[-_\s]*client)\s*:?\b',
r'\b(?:courriel[-_\s]*client|courriel[-_\s]*utilisateur|mail[-_\s]*client)\s*:?\b',
# Italian variations - only labels
r'\b(?:posta[-_\s]*elettronica|e[-_\s]*mail|indirizzo[-_\s]*e[-_\s]*mail)\s*:?\b',
r'\b(?:email[-_\s]*cliente|email[-_\s]*utente)\s*:?\b',
r'\b(?:mail[-_\s]*cliente|mail[-_\s]*utente|posta[-_\s]*cliente)\s*:?\b'
],
replacement_template="[EMAIL_{}]"
),
# Phone patterns
Pattern(
name="phone",
patterns=[
# Simple variations
r'\b(?:phone|tel|telephone|mobile)\b',
r'\b(?:contact[-_\s]*number|phone[-_\s]*number|tel[-_\s]*number)\b',
# German variations
r'\b(?:telefon|mobil|handy|telefon[-_\s]*nummer)\b',
r'\b(?:kontakt[-_\s]*nummer|telefon[-_\s]*nummer|tel[-_\s]*nummer)\b',
# French variations
r'\b(?:téléphone|portable|mobile|numéro[-_\s]*de[-_\s]*téléphone)\b',
r'\b(?:numéro[-_\s]*de[-_\s]*contact|tél[-_\s]*fixe|tél[-_\s]*mobile)\b',
# Italian variations
r'\b(?:telefono|cellulare|mobile|numero[-_\s]*di[-_\s]*telefono)\b',
r'\b(?:numero[-_\s]*di[-_\s]*contatto|tel[-_\s]*fisso|tel[-_\s]*mobile)\b'
],
replacement_template="[PHONE_{}]"
),
# IBAN patterns
Pattern(
name="iban",
patterns=[
# Simple variations
r'\b(?:iban|bank[-_\s]*account|account[-_\s]*number)\b',
r'\b(?:bank[-_\s]*details|account[-_\s]*details|banking[-_\s]*info)\b',
# German variations
r'\b(?:iban|bank[-_\s]*konto|konto[-_\s]*nummer)\b',
r'\b(?:bank[-_\s]*verbindung|konto[-_\s]*verbindung|bank[-_\s]*daten)\b',
# French variations
r'\b(?:iban|compte[-_\s]*bancaire|numéro[-_\s]*de[-_\s]*compte)\b',
r'\b(?:coordonnées[-_\s]*bancaires|détails[-_\s]*bancaires)\b',
# Credit card variations in French
r'\b(?:carte[-_\s]*de[-_\s]*credit|carte[-_\s]*credit|numero[-_\s]*carte[-_\s]*credit)\b',
r'\b(?:carte[-_\s]*bancaire|carte[-_\s]*de[-_\s]*paiement)\b',
r'\b(?:carte[-_\s]*de[-_\s]*crédit|carte[-_\s]*crédit|numéro[-_\s]*carte[-_\s]*crédit)\b',
r'\b(?:carte[-_\s]*de[-_\s]*débit|carte[-_\s]*débit|numéro[-_\s]*carte[-_\s]*débit)\b',
# Italian variations
r'\b(?:iban|conto[-_\s]*bancario|numero[-_\s]*di[-_\s]*conto)\b',
r'\b(?:coordinate[-_\s]*bancarie|dettagli[-_\s]*bancari)\b',
# Common variations
r'\b(?:bankkonto|bank[-_\s]*konto|conto[-_\s]*di[-_\s]*banca)\b',
# Credit card variations
r'\b(?:credit[-_\s]*card|credit[-_\s]*card[-_\s]*number|credit[-_\s]*card[-_\s]*no)\b',
r'\b(?:credit[-_\s]*card[-_\s]*nr|credit[-_\s]*card[-_\s]*num)\b',
r'\b(?:credit[-_\s]*card[-_\s]*id|credit[-_\s]*card[-_\s]*code)\b',
r'\b(?:credit[-_\s]*card[-_\s]*reference|credit[-_\s]*card[-_\s]*ref)\b',
r'\b(?:credit[-_\s]*card[-_\s]*details|credit[-_\s]*card[-_\s]*info)\b',
r'\b(?:credit[-_\s]*card[-_\s]*data|credit[-_\s]*card[-_\s]*account)\b',
# Credit card variations in other languages
r'\b(?:kredit[-_\s]*karte|kreditkarte|kredit[-_\s]*karten[-_\s]*nummer)\b',
r'\b(?:carta[-_\s]*di[-_\s]*credito|carta[-_\s]*credito|numero[-_\s]*carta[-_\s]*credito)\b',
# Payment variations
r'\b(?:payment[-_\s]*details|payment[-_\s]*info|payment[-_\s]*data)\b',
r'\b(?:zahlungs[-_\s]*details|zahlungs[-_\s]*informationen|zahlungs[-_\s]*daten)\b',
r'\b(?:détails[-_\s]*de[-_\s]*paiement|informations[-_\s]*de[-_\s]*paiement)\b',
r'\b(?:dettagli[-_\s]*di[-_\s]*pagamento|informazioni[-_\s]*di[-_\s]*pagamento)\b',
# Common credit card abbreviations
r'\b(?:cc[-_\s]*number|cc[-_\s]*no|cc[-_\s]*nr)\b',
r'\b(?:cc[-_\s]*num|cc[-_\s]*id|cc[-_\s]*code)\b',
r'\b(?:cc[-_\s]*ref|cc[-_\s]*details|cc[-_\s]*info)\b',
r'\b(?:cc[-_\s]*data|cc[-_\s]*account)\b',
# Simple credit card
r'\b(?:credit[-_\s]*card|credit[-_\s]*card[-_\s]*number)\b',
# Additional credit card variations
r'\b(?:card[-_\s]*number|card[-_\s]*no|card[-_\s]*nr)\b',
r'\b(?:card[-_\s]*num|card[-_\s]*id|card[-_\s]*code)\b',
r'\b(?:card[-_\s]*ref|card[-_\s]*details|card[-_\s]*info)\b',
r'\b(?:card[-_\s]*data|card[-_\s]*account)\b'
],
replacement_template="[IBAN_{}]"
),
# Address patterns
Pattern(
name="address",
patterns=[
# English variations
r'\b(?:address|street[-_\s]*address|mailing[-_\s]*address)\b',
r'\b(?:home[-_\s]*address|work[-_\s]*address|billing[-_\s]*address)\b',
r'\b(?:.*address.*)\b', # Match any text containing "address"
# German variations
r'\b(?:adresse|strassen[-_\s]*adresse|post[-_\s]*adresse)\b',
r'\b(?:wohn[-_\s]*adresse|geschäfts[-_\s]*adresse|rechnungs[-_\s]*adresse)\b',
r'\b(?:.*adresse.*)\b', # Match any text containing "adresse"
# French variations
r'\b(?:adresse|adresse[-_\s]*postale|adresse[-_\s]*de[-_\s]*livraison)\b',
r'\b(?:adresse[-_\s]*personnelle|adresse[-_\s]*professionnelle)\b',
r'\b(?:.*adresse.*)\b', # Match any text containing "adresse"
# Italian variations
r'\b(?:indirizzo|indirizzo[-_\s]*postale|indirizzo[-_\s]*di[-_\s]*consegna)\b',
r'\b(?:indirizzo[-_\s]*personale|indirizzo[-_\s]*professionale)\b',
r'\b(?:.*indirizzo.*)\b', # Match any text containing "indirizzo"
# Common variations
r'\b(?:location|place|residence|domicile)\b',
r'\b(?:standort|ort|wohnort|domizil)\b',
r'\b(?:lieu|emplacement|résidence|domicile)\b',
r'\b(?:luogo|posizione|residenza|domicilio)\b'
],
replacement_template="[ADDRESS_{}]"
),
# Date patterns
Pattern(
name="date",
patterns=[
# English variations
r'\b(?:date|birth[-_\s]*date|date[-_\s]*of[-_\s]*birth)\b',
r'\b(?:dob|birthday|anniversary)\b',
# German variations
r'\b(?:datum|geburt[-_\s]*datum|geboren[-_\s]*am)\b',
r'\b(?:geburtstag|jubiläum|feier[-_\s]*tag)\b',
r'\b(?:geboren|geb\.|geboren[-_\s]*am)\b',
# French variations
r'\b(?:date|date[-_\s]*de[-_\s]*naissance|né[-_\s]*le)\b',
r'\b(?:anniversaire|date[-_\s]*anniversaire)\b',
r'\b(?:né|née|né[-_\s]*le)\b',
# Italian variations
r'\b(?:data|data[-_\s]*di[-_\s]*nascita|nato[-_\s]*il)\b',
r'\b(?:compleanno|anniversario)\b',
r'\b(?:nato|nata|nato[-_\s]*il)\b',
# Common variations
r'\b(?:birth|born|geboren|né|nato)\b'
],
replacement_template="[DATE_{}]"
),
# SSN patterns
Pattern(
name="ssn",
patterns=[
# English variations
r'\b(?:ssn|social[-_\s]*security[-_\s]*number|tax[-_\s]*id)\b',
r'\b(?:tax[-_\s]*identification|national[-_\s]*id)\b',
# German variations
r'\b(?:ahv[-_\s]*nummer|sozial[-_\s]*versicherungs[-_\s]*nummer)\b',
r'\b(?:steuer[-_\s]*nummer|steuer[-_\s]*id|svn)\b',
r'\b(?:ahv[-_\s]*nr|ahv[-_\s]*no|ahv[-_\s]*num)\b',
# French variations
r'\b(?:numéro[-_\s]*avs|numéro[-_\s]*de[-_\s]*sécurité[-_\s]*sociale)\b',
r'\b(?:numéro[-_\s]*fiscal|numéro[-_\s]*d\'identification)\b',
# Italian variations
r'\b(?:numero[-_\s]*avs|numero[-_\s]*di[-_\s]*sicurezza[-_\s]*sociale)\b',
r'\b(?:numero[-_\s]*fiscale|codice[-_\s]*fiscale)\b',
# Common variations
r'\b(?:ahv|svn|nss|avs)\b',
# Additional AHV variations
r'\b(?:ahv_nummer|ahvnummer|ahv-nummer|ahv_number)\b',
r'\b(?:ahv[-_\s]*nr|ahv[-_\s]*no|ahv[-_\s]*num)\b',
r'\b(?:ahv[-_\s]*number|ahv[-_\s]*number)\b',
r'\b(?:ahv[-_\s]*id|ahv[-_\s]*id)\b',
r'\b(?:ahv[-_\s]*code|ahv[-_\s]*code)\b',
r'\b(?:ahv[-_\s]*reference|ahv[-_\s]*reference)\b',
r'\b(?:ahv[-_\s]*reference[-_\s]*number|ahv[-_\s]*reference[-_\s]*number)\b',
r'\b(?:ahv[-_\s]*reference[-_\s]*no|ahv[-_\s]*reference[-_\s]*no)\b',
r'\b(?:ahv[-_\s]*reference[-_\s]*nr|ahv[-_\s]*reference[-_\s]*nr)\b',
r'\b(?:ahv[-_\s]*reference[-_\s]*num|ahv[-_\s]*reference[-_\s]*num)\b',
r'\b(?:ahv[-_\s]*reference[-_\s]*id|ahv[-_\s]*reference[-_\s]*id)\b',
r'\b(?:ahv[-_\s]*reference[-_\s]*code|ahv[-_\s]*reference[-_\s]*code)\b'
],
replacement_template="[SSN_{}]"
)
]
class DataPatterns:
"""Patterns for identifying sensitive data in content"""
patterns = [
# Name patterns
Pattern(
name="name",
patterns=[
# Person names with titles and academic degrees
r'\b(?:Dr\.|Prof\.|PhD\.?|MD\.?|Herr|Frau|Mr\.|Mrs\.|Ms\.|Monsieur|Madame|Signore|Signora)\s+[A-Z][a-z]{2,}(?:\s+[A-Za-z]{2,}){1,2}\b'
],
replacement_template="[NAME_{}]"
),
# Email pattern for plain text
Pattern(
name="email",
patterns=[
# Basic email pattern
r'[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*'
],
replacement_template="[EMAIL_{}]"
),
# Phone patterns
Pattern(
name="phone",
patterns=[
# International format
r'\+\d{1,3}[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,9}\b',
# Swiss format
r'\b(?:0\d{1,2}|0041\d{1,2})[-.\s]?\d{3}[-.\s]?\d{2}[-.\s]?\d{2}\b',
# German format
r'\b(?:0\d{1,4}|0049\d{1,4})[-.\s]?\d{3,}[-.\s]?\d{3,}\b',
# French format
r'\b(?:0\d{1,2}|0033\d{1,2})[-.\s]?\d{1,2}[-.\s]?\d{2}[-.\s]?\d{2}[-.\s]?\d{2}\b',
# Italian format
r'\b(?:0\d{1,3}|0039\d{1,3})[-.\s]?\d{3,}[-.\s]?\d{3,}\b',
# Mobile numbers
r'\b(?:07|00417|004917|00337|00397)\d{8,9}\b',
# Emergency numbers
r'\b(?:112|911|118|117|144|1414)\b'
],
replacement_template="[PHONE_{}]"
),
# IBAN patterns
Pattern(
name="iban",
patterns=[
r'\b(?:CH|DE|FR|IT)\d{2}\s?(?:\d{4}\s?){5}\d{2}\b',
r'\b(?:CH|DE|FR|IT)\d{2}(?:\d{4}){5}\d{2}\b'
],
replacement_template="[IBAN_{}]"
),
# Address patterns
Pattern(
name="address",
patterns=[
r'\b(?:[A-Za-zäöüßÄÖÜ]+(?:strasse|str\.|gasse|weg|platz|allee|boulevard|avenue|via|strada|rue|chemin|route))\s+\d{1,4}(?:[a-z])?\b',
r'\b\d{4}\s+[A-Za-zäöüßÄÖÜ]+\b'
],
replacement_template="[ADDRESS_{}]"
),
# Date patterns
Pattern(
name="date",
patterns=[
# Specific date formats with context
r'\b(?:geboren|birth|né|nato)\s+am\s+[0-9]{2}[./-][0-9]{2}[./-][0-9]{4}\b', # Birth dates
r'\b(?:geboren|birth|né|nato)\s+am\s+[0-9]{4}[./-][0-9]{2}[./-][0-9]{2}\b', # Birth dates
r'\b(?:vertrag|contract|contrat|contratto)\s+vom\s+[0-9]{2}[./-][0-9]{2}[./-][0-9]{4}\b', # Contract dates
r'\b(?:vertrag|contract|contrat|contratto)\s+vom\s+[0-9]{4}[./-][0-9]{2}[./-][0-9]{2}\b', # Contract dates
# Specific date formats with month names
r'\b(?:geboren|birth|né|nato)\s+am\s+(?:jan|feb|mar|apr|mai|jun|jul|aug|sep|okt|nov|dez|januar|februar|märz|april|mai|juni|juli|august|september|oktober|november|dezember)[a-z]*\s+\d{4}\b', # Birth dates with month
r'\b(?:vertrag|contract|contrat|contratto)\s+vom\s+(?:jan|feb|mar|apr|mai|jun|jul|aug|sep|okt|nov|dez|januar|februar|märz|april|mai|juni|juli|august|september|oktober|november|dezember)[a-z]*\s+\d{4}\b' # Contract dates with month
],
replacement_template="[DATE_{}]"
),
# SSN patterns
Pattern(
name="ssn",
patterns=[
r'\b(?:756|757|758|759)\.\d{4}\.\d{4}\.\d{2}\b', # Swiss AHV
r'\b(?:CHE|DE|FR|IT)-\d{3}\.\d{3}\.\d{3}\b', # Company IDs
r'\b\d{3}\.\d{3}\.\d{3}\b' # Generic SSN format
],
replacement_template="[SSN_{}]"
)
]
class TextTablePatterns:
"""Patterns for identifying table-like structures in text"""
@staticmethod
def get_patterns() -> List[Tuple[str, str]]:
return [
# key: value pattern (with optional whitespace)
(r'^([^:]+):\s*(.+)$', ':'),
# key = value pattern (with optional whitespace)
(r'^([^=]+)=\s*(.+)$', '='),
# key = value pattern (with required whitespace)
(r'^([^=]+)\s+=\s+(.+)$', '='),
# key: value pattern (with required whitespace)
(r'^([^:]+)\s+:\s+(.+)$', ':'),
]
@staticmethod
def is_table_line(line: str) -> bool:
"""Check if a line matches any table pattern"""
patterns = TextTablePatterns.get_patterns()
return any(re.match(pattern[0], line.strip()) for pattern in patterns)
@staticmethod
def extract_key_value(line: str) -> Optional[Tuple[str, str]]:
"""Extract key and value from a table line"""
patterns = TextTablePatterns.get_patterns()
for pattern, separator in patterns:
match = re.match(pattern, line.strip())
if match:
key = match.group(1).strip()
value = match.group(2).strip()
return key, value
return None
def get_pattern_for_header(header: str, patterns: List[Pattern]) -> Optional[Pattern]:
"""
Find matching pattern for a header
Args:
header: The header to check
patterns: List of patterns to check against
Returns:
Optional[Pattern]: Matching pattern or None
"""
if not header:
return None
header = header.lower().strip()
for pattern in patterns:
for p in pattern.patterns:
if re.search(p, header, re.IGNORECASE):
return pattern
return None
def find_patterns_in_text(text: str, patterns: List[Pattern]) -> List[tuple]:
"""
Find all pattern matches in text
Args:
text: Text to search
patterns: List of patterns to check
Returns:
List[tuple]: List of (pattern_name, match, start, end)
"""
matches = []
for pattern in patterns:
for p in pattern.patterns:
matches.append((pattern.name, match.group(0), match.start(), match.end()))
return sorted(matches, key=lambda x: x[2]) # Sort by start position

View file

@ -1,608 +0,0 @@
# Bewertung der PowerOn AI Platform
Basierend auf dem nachstehenden Q&A ergibt sich nachfolgende Bewertung des Softwareprodukts "PowerOn AI Platform".
## Aktueller Wert per Juni 2025
1. **Technischer Wert des Codes**:
- Professionelle, modulare Codebasis (~50.000-60.000 LOC)
- Moderne Architektur mit innovativen Komponenten
- 5 Personenmonate Entwicklung x CHF 15.000/PM = CHF 75.000
- Zusätzlicher Wert durch Enterprise-Ready Architektur: CHF 50.000
2. **Bisherige Investitionen**:
- Hardware/Software: CHF 20.000
- Expertise-Premium (30+ Jahre Erfahrung): CHF 25.000
3. **IP und Innovationswert**:
- Multi-Agent Workflow-System
- Modulare Architecture mit Alleinstellungsmerkmalen
- Geschätzter Wert: CHF 100.000
4. **Marktpotenzial-Faktor**:
- Adressierbares Marktvolumen von CHF 500-700 Mio.
- Wachstumsmarkt (25-30% jährlich)
- Frühphasen-Multiplikator: 2x
**Aktueller Gesamtwert (Juni 2025)**: **CHF 500.000**
## Prognostizierte Wertentwicklung
### Ende 2025
- Abschluss der technischen Entwicklung
- Erste Pilotprojekte mit 3-5 Referenzkunden
- Validierung des Produkts am Markt
- **Geschätzter Wert Ende 2025**: **CHF 1,2 Millionen**
(Steigerung durch Marktvalidierung und Risikoreduktion)
### Ende 2026
- 20-30 Kunden
- ARR: CHF 0,5-0,8 Mio.
- Etablierung im DACH-Markt
- **Geschätzter Wert Ende 2026**: **CHF 4-5 Millionen**
(Bewertungsmultiplikator von 6-8x ARR für wachstumsstarke SaaS)
### Ende 2027
- 70-90 Kunden
- ARR: CHF 2-2,5 Mio.
- Erweiterung der Produktpalette
- **Geschätzter Wert Ende 2027**: **CHF 12-15 Millionen**
(Bewertungsmultiplikator von 6x ARR)
### Ende 2028
- 150+ Kunden
- ARR: CHF 4,5 Mio.
- Internationale Expansion
- **Geschätzter Wert Ende 2028**: **CHF 25-30 Millionen**
(Bewertungsmultiplikator von 5,5-6,5x ARR für etablierte SaaS)
## Schlüsselfaktoren und Risikobeurteilung für die Wertentwicklung
1. **Erfolgreiche Markteinführung**: Der Übergang von Entwicklung zu erfolgreicher Pilotphase ist kritisch für die Wertentwicklung 2025-2026.
2. **Skalierung des Vertriebs**: Die Fähigkeit, die Kundenakquisition gemäss der Prognose zu skalieren, ist entscheidend für die 2026-2027 Bewertung.
3. **Kapitaleffizienz**: Die effiziente Nutzung des Kapitals (CHF 750.000-1.050.000) für die nächste Entwicklungsphase wird die Bewertung massgeblich beeinflussen.
4. **Marktdynamik**: Die Entwicklung des KI-Marktes und regulatorische Änderungen können sowohl positive als auch negative Auswirkungen haben.
Diese Bewertung basiert auf der Annahme, dass die Meilensteine wie geplant erreicht werden und keine signifikanten externen Faktoren die Marktentwicklung negativ beeinflussen.
# Kriterienkatalog zur Softwarebewertung
## Teil 1: Technische Bewertung (Code-basiert)
1. Wie umfangreich ist die Codebasis (LOC, Module, Komponenten)?
2. Welche Programmiersprachen und Frameworks wurden verwendet?
3. Wie hoch ist die Codequalität und -konsistenz (saubere Architektur, Dokumentation, Tests)?
4. Gibt es innovative Algorithmen oder patentierbare technische Lösungen?
5. Wie modular und wartbar ist die Software gestaltet?
6. Wie robust ist die Fehlerbehandlung und Sicherheitsarchitektur?
7. Wie skalierbar ist die technische Infrastruktur?
8. Gibt es technische Schulden, die zukünftige Entwicklungen behindern könnten?
### 1. Umfang der Codebasis
- **Frontend**: Modulare JavaScript-Struktur mit ca. 15 Hauptmodulen
- **Backend**: Python/FastAPI mit ca. 15 Hauptmodulen
- **Hauptkomponenten**:
- Frontend: Workflow, UI, Koordination, Datenmanagement
- Backend: Gateway, Agent Service, Connectors, Workflow Manager
- **Geschätzte LOC**: ~50,000-60,000 Zeilen Code
### 2. Programmiersprachen und Frameworks
- **Frontend**:
- JavaScript (ES6+)
- Modulares System mit ES6-Import/Export
- Vanilla JS ohne externe Frameworks
- **Backend**:
- Python 3.x
- FastAPI für REST-API
- Asyncio für asynchrone Verarbeitung
### 3. Codequalität und -konsistenz
- **Saubere Architektur**:
- Klare Trennung Frontend/Backend
- Modulare Struktur mit definierten Verantwortlichkeiten
- State Machine Pattern für Workflow-Management
- **Dokumentation**:
- Ausführliche JSDoc/Python-Docstrings
- Architekturdiagramme (Mermaid)
- Technische Spezifikationen
- **Tests**:
- Automatisierte Modultests
- Manuell Integrationstests
- Benutzertests über Tickets in Clickup
### 4. Innovative Algorithmen/patentierbare Lösungen
- Multi-Agent Workflow-System mit spezialisierten Agenten
- Modulares Agent-Registry-System
- State Machine für Workflow-Koordination
- Dynamische Agenten-Integration
### 5. Modularität und Wartbarkeit
- **Hohe Modularität**:
- Klare Trennung der Verantwortlichkeiten
- Plug-and-Play Agent-System
- Erweiterbare Connector-Architektur
- **Wartbarkeit**:
- Konsistente Codestruktur
- Klare Namenskonventionen
- Dokumentierte Schnittstellen
### 6. Fehlerbehandlung und Sicherheit
- **Robuste Fehlerbehandlung**:
- State Machine für Workflow-Status
- Exception Handling auf allen Ebenen
- Logging-System für Debugging
- **Sicherheitsarchitektur**:
- Multi-Tenant-Architektur
- Authentifizierung/Autorisierung
- Mandantenverwaltung
### 7. Skalierbarkeit
- **Horizontale Skalierbarkeit**:
- Modulare Architektur
- Asynchrone Verarbeitung
- Connector-System für externe Dienste
- **Vertikale Skalierbarkeit**:
- Workflow-Parallelisierung
- Agent-Pooling
- Caching-Mechanismen
### 8. Technische Schulden
- **Potenzielle Verbesserungsbereiche**:
- Test-Coverage nicht sichtbar
- Eventuell fehlende Performance-Optimierungen
- Dokumentation für Zielgruppen noch unvollständig
- **Keine kritischen Blockierer identifiziert**
### Fazit der technischen Bewertung
Die Codebasis zeigt eine professionelle, gut strukturierte Enterprise-Anwendung mit klarer Architektur und modernen Best Practices. Die modulare Struktur und die saubere Implementierung der State Machine für Workflow-Management sind besonders hervorzuheben. Die Anwendung ist technisch reif und zeigt ein hohes Mass an Professionalität in der Implementierung.
## Teil 2: Bewertung der bisherigen Aufwände
### 1. Entwicklungsaufwände
- **Patrick**:
- ValueOn AG: 80 Stunden
- Private Entwicklung: 650 Stunden
- **Gesamt**: 730 Stunden (ca. 4.5 Personenmonate)
- **Ida**:
- ValueOn AG: 60 Stunden
- **Gesamt**: 60 Stunden (ca. 0.4 Personenmonate)
- **Gesamtaufwand**: ~5 Personenmonate
### 2. Qualifikationen und Erfahrungslevel
- **Patrick**:
- Business Consultant
- Software Architect
- Full Stack Developer
- 30+ Jahre Berufserfahrung
- Experte für Enterprise-Architekturen
- **Ida**:
- Business Analyst
- Project Manager
- Scrum Master
- Erfahrung in agiler Entwicklung
- Expertise in Prozessoptimierung
### 3. Spezifische Fachkenntnisse
- **Patrick**:
- Umfassende Markt- und Business-Erfahrung (30 Jahre)
- Expertise in Software-Architektur
- Full Stack Entwicklung
- Azure Cloud-Integration
- KI/ML Integration
- Enterprise-Systeme
- **Ida**:
- Projektmanagement
- Agile Methoden
- Business Analysis
- Prozessoptimierung
- Qualitätssicherung
### 4. Finanzielle Investitionen
- **Hardware**: CHF 10,000
- Entwicklungsserver
- Testumgebungen
- Entwicklungshardware
- **Software & Lizenzen**: CHF 10,000
- Entwicklungstools
- Cloud-Services
- KI-API-Zugänge
- **Gesamt**: CHF 20,000
### 5. Externe Dienstleister
- **Aktueller Status**: Keine externen Dienstleister
- **Vorteile**:
- Volle Kontrolle über Entwicklung
- Tiefes Verständnis der Architektur
- Schnelle Entscheidungswege
- Kosteneffizienz
### 6. Schlüsselkomponenten-Entwicklung
- **Patrick**:
- Frontend-Architektur
- Backend-System
- Workflow-Engine
- Agent-System
- Connector-Framework
- Datenmanagement
- Sicherheitsarchitektur
- **Verantwortlichkeiten**:
- Systemarchitektur
- Technische Leitung
- Code-Review
- Qualitätssicherung
### 7. Nicht-monetäre Ressourcen
- **Dominic**:
- Umfangreiches Sales & Marketing Netzwerk
- Marktzugang
- Branchenkontakte
- **ValueOn AG**:
- Infrastruktur
- Rechtlicher Rahmen
- Geschäftsprozesse
- **Netzwerke**:
- Technologie-Partner
- Potenzielle Kunden
- Branchenexperten
### 8. Finanzielle Risiken
- **ValueOn AG**:
- Bereitstellung von Infrastruktur
- Personelle Ressourcen
- Rechtlicher Rahmen
- **Private Investitionen**:
- Entwicklungszeit
- Hardware/Software
- Cloud-Services
### Fazit bisherige Aufwände
Die bisherigen Aufwände zeigen ein ausgewogenes Verhältnis zwischen technischer Expertise und Business-Know-how. Die private Investition von 730 Stunden durch Patrick demonstriert ein hohes Engagement und tiefes Verständnis der Technologie. Die Kombination aus technischer Expertise, Business-Erfahrung und Marktzugang bildet eine solide Grundlage für die weitere Entwicklung. Die bisherigen Investitionen sind effizient eingesetzt worden, mit Fokus auf kritische Kernkomponenten und skalierbare Architektur.
## Teil 3: Markt- und Geschäftspotenzial
### 1. Adressierbarer Gesamtmarkt und Wachstumspotenzial
- **Gesamtmarktvolumen 2025**:
- KI-Markt: $190 Mrd.
- Business Process Automation: $19,6 Mrd.
- Enterprise Knowledge Management: $43 Mrd.
- **Adressierbarer Markt (SAM)**:
- Initial: Mittlerer Markt in DACH (Professional Services, Finanzdienstleistungen, Gesundheitswesen)
- Geschätztes SAM: CHF 500-700 Mio.
- **Wachstumspotenzial**:
- Jährliches Marktwachstum: 25-30%
- Erweiterung auf internationale Märkte
- Branchenspezifische Lösungen
### 2. Alleinstellungsmerkmale
1. **Technologische Vorteile**:
- Proprietäre Multi-Agent-Technologie
- Modellunabhängige KI-Integration
- Enterprise-Ready Architektur
- Fortschrittliche Workflow-Orchestrierung
2. **Funktionale Vorteile**:
- Nahtlose Integration verschiedener KI-Modelle
- Robuste Fehlerbehandlung
- Skalierbare Multi-Tenant-Architektur
- Umfassende Enterprise-Features
### 3. Kunden und Pilotprojekte
- **Aktueller Status**:
- In Entwicklung
- Erste Referenzkunden in Planung
- Fokus auf mittelständische Unternehmen
- **Pilotphase**:
- Geplant für Q3/Q4 2025
- 3-5 Schlüsselreferenzkunden
- Branchenspezifische Templates
### 4. Geschäftsmodell
- **Hauptmodell**: SaaS (Software as a Service)
- **Preismodell**:
- Basis: Pro-Benutzer/Monat Abonnement
- Zusätzlich: Nutzungsbasierte Abrechnung
- Enterprise-Lizenzen für grössere Kunden
- **Erwartete Margen**: 75-85% nach Skalierung
### 5. Preisgestaltung
- **Wettbewerbsvergleich**:
- Unterhalb Enterprise-Lösungen
- Über Standard-BPA-Tools
- Flexiblere Preisgestaltung als Konkurrenz
- **Preisstruktur**:
- Basis-Abonnement: CHF 50-100 pro Benutzer/Monat
- Nutzungsbasierte Komponente: CHF 0.10-0.50 pro Verarbeitungseinheit
- Enterprise-Pakete: Individuelle Preisgestaltung
### 6. Umsatzpotenziale
- **Jahr 1 (2026)**:
- Ziel: 20-30 Kunden
- Erwartetes ARR: CHF 0.5-0.8 Mio.
- **Jahr 2 (2027)**:
- Ziel: 70-90 Kunden
- Erwartetes ARR: CHF 2-2.5 Mio.
- **Jahr 3 (2028)**:
- Ziel: 150+ Kunden
- Erwartetes ARR: CHF 4.5 Mio.
### 7. Akquisitionskosten
- **Customer Acquisition Cost (CAC)**:
- Erwarteter CAC: CHF 15,000-20,000
- Payback-Zeit: 12-18 Monate
- **Kostenstruktur**:
- 30% Vertrieb und Marketing
- Fokus auf effiziente Akquisition
- Skaleneffekte ab 50+ Kunden
### 8. Regulatorische Herausforderungen
- **Datenschutz**:
- DSGVO-Konformität
- Datensicherheit
- Mandantentrennung
- **KI-Regulierung**:
- EU AI Act
- Transparenzpflichten
- Qualitätssicherung
- **Branchenspezifische Regulierung**:
- Finanzdienstleistungen
- Gesundheitswesen
- Professional Services
### Fazit Markt- und Geschäftspotenzial
Die PowerOn AI Platform adressiert einen wachsenden Markt mit klaren Alleinstellungsmerkmalen. Das Geschäftsmodell ist skalierbar und die Preisgestaltung wettbewerbsfähig. Die regulatorischen Herausforderungen sind bekannt und adressierbar. Die Umsatzprognosen sind konservativ kalkuliert und basieren auf realistischen Marktannahmen.
## Teil 4: Skalierungs- und Zukunftspotenzial
### 1. Ressourcen für Support und Weiterentwicklung
- **Entwicklungsteam**:
- 2-3 Full-Stack Entwickler, KI-unterstützte Entwicklung
- 1 DevOps Engineer
- 1 QA Engineer
- **Support-Team**:
- 1-2 Support Engineers
- 1 Technical Account Manager
- **Infrastruktur**:
- Cloud-basierte Skalierung (Azure)
- Automatisierte Deployment-Pipeline
- Monitoring und Logging-Systeme
### 2. Skalierbarkeit
- **Technische Skalierbarkeit**:
- Horizontale Skalierung durch Multi-Tenant-Architektur
- Vertikale Skalierung durch Agent-Pooling
- Automatische Lastverteilung
- **Skalierungszeitrahmen**:
- 2x Nutzer: Sofort möglich
- 5x Nutzer: 1-2 Monate Vorbereitung
- 10x Nutzer: 3-4 Monate mit Infrastruktur-Erweiterung
### 3. Kapitalbedarf
- **Nächste Entwicklungsphase (12 Monate)**:
- Entwicklung: CHF 400,000-500,000
- Marketing & Sales: CHF 100,000-200,000
- Infrastruktur: CHF 50,000-100,000
- Betrieb & Support: CHF 100,000-150,000
- **Gesamt**: CHF 750,000-1,050,000
### 4. Schlüsselpersonen
- **Technische Leitung**:
- Patrick (Software Architect, Full Stack Developer)
- Verantwortlich für: Architektur, Entwicklung, Technische Strategie
- **Business & Operations**:
- Ida (Business Analyst, Project Manager)
- Verantwortlich für: Projektmanagement, Business Analysis
- **Sales & Marketing**:
- Dominic (Sales & Marketing)
- Verantwortlich für: Marktentwicklung, Kundenakquisition
### 5. Exit-Strategien
- **Strategische Übernahme**:
- Enterprise Software Anbieter
- KI/ML Plattform Betreiber
- Business Process Automation Unternehmen
- **IPO-Potenzial**:
- Ab CHF 50 Mio. ARR
- Zeitrahmen: 5-7 Jahre
- **Extraktion aus ValueOn AG**:
- Vergütung der Aufwände
- Anrechnung des Mehrwerts für Schlüsselpersonen
- Beschaffung des notwendigen Kapitals
### 6. Strategische Partnerschaften
- **Technologie-Partner**:
- KI-Provider (OpenAI, Anthropic)
- Cloud-Provider (Azure)
- Enterprise Software Anbieter
- **Vertriebspartner**:
- Systemhäuser
- Beratungsunternehmen
- Branchenspezialisten
- **Forschungspartner**:
- Universitäten
- Forschungsinstitute
- KI-Labore
### 7. Geplante Erweiterungen
- **Kurzfristig (12 Monate)**:
- Erweiterte Agent-Typen
- Branchenspezifische Templates
- API-Erweiterungen
- **Mittelfristig (24 Monate)**:
- Agentenmarktplatz
- Proprietäre KI-Modelle
- Erweiterte Analytics
- **Langfristig (36+ Monate)**:
- KI-Middleware für Unternehmen
- Branchenlösungen
- Internationale Expansion
### 8. Langfristige Vision
- **Technologische Vision**:
- Führende Multi-Agent KI-Plattform
- Standard für Enterprise Workflow Automation
- Innovationstreiber in der KI-Integration
- **Marktvision**:
- Globaler Marktführer in Nischenbereichen
- Branchenstandard für bestimmte Anwendungsfälle
- Referenz für KI-gestützte Prozessoptimierung
- **Geschäftsvision**:
- Nachhaltiges Wachstum
- Profitables Geschäftsmodell
- Führende Position in ausgewählten Märkten
### Fazit Skalierungs- und Zukunftspotenzial
Die PowerOn AI Platform verfügt über ein solides Skalierungspotenzial sowohl technisch als auch geschäftlich. Die modulare Architektur ermöglicht schnelles Wachstum, während die klare Vision und die strategischen Partnerschaften den langfristigen Erfolg unterstützen. Die Kapitalanforderungen sind realistisch kalkuliert und die Exit-Strategien bieten verschiedene Optionen für die Zukunft.
# Exitplan: PowerOn AI Platform als eigenständige AG
## Bewertung und Ausgangssituation
**Aktueller Wert (Juni 2025)**: CHF 500.000
**Angepasster Gründungswert**: CHF 800.000 (berücksichtigt den strategischen Wert der Produktvision, welche bereits als innerer Wert im Produkt enthalten ist)
## Strukturierung der PowerOn AG
### 1. Aktienstruktur bei Gründung
**Gesamtes Aktienkapital**: 1.000.000 Aktien (Nennwert CHF 0,10)
**Firmenvaluation bei Gründung**: CHF 800.000
#### Verteilung der initialen Aktien:
- **Patrick**:
- Eingebrachte Leistung: Entwicklung, technische Expertise und essenzielles Gesamtkonzept
- **Aktienanteil**: 35% (350.000 Aktien)
- **Dominic**:
- Eingebrachte Leistung: Netzwerk, Sales & Marketing Expertise und Vision
- **Aktienanteil**: 15% (150.000 Aktien)
- **ValueOn AG**:
- Eingebrachte Leistung: Infrastruktur, rechtlicher Rahmen, Arbeitszeit, Übertragung von IP
- **Aktienanteil**: 25% (250.000 Aktien)
- **Reservierter Anteil für Mitarbeiter-Pool**:
- **Aktienanteil**: 10% (100.000 Aktien)
- **Reserviert für Investoren (erste Runde)**:
- **Aktienanteil**: 15% (150.000 Aktien)
### 2. Kapitalbedarfsplanung (18 Monate)
| Kategorie | Betrag (CHF) |
|-----------|--------------|
| Entwicklung | 550.000 |
| Marketing & Sales | 250.000 |
| Infrastruktur | 100.000 |
| Betrieb & Support | 200.000 |
| **Gesamtbedarf** | **1.100.000** |
**Kapitalbeschaffungsstrategie**:
- **Erste Finanzierungsrunde**: CHF 1.000.000 (für 18 Monate)
- **Sicherheitspuffer**: CHF 100.000 (aus Umsätzen/zukünftigen Einnahmen)
### 3. Investitionskonditionen
**Pre-Money Bewertung**: CHF 800.000
**Investitionsvolumen**: CHF 1.000.000
**Post-Money Bewertung**: CHF 1.800.000
**Aktienkurs für Investoren**:
- 150.000 bestehende Aktien + 214.285 neue Aktien = 364.285 Aktien für Investoren
- **Aktienkurs**: CHF 2,75 pro Aktie
**Aktienstruktur nach Investment**:
- Patrick: 35% → 28,9% (350.000 Aktien)
- Dominic: 15% → 12,4% (150.000 Aktien)
- ValueOn AG: 25% → 20,7% (250.000 Aktien)
- Mitarbeiter-Pool: 10% → 8,3% (100.000 Aktien)
- Investoren: 29,7% (364.285 Aktien)
## Governance und Organisation
### 1. Schlüsselpositionen in der Organisation
- **CEO**: Gesamtführung des Unternehmens
- **CTO**: Verantwortlich für technische Strategie und Produktentwicklung
- **CSO/Vertriebsleitung**: Verantwortlich für Vertrieb und Marktentwicklung
- **COO**: Operative Leitung und Geschäftsprozesse
Die Besetzung dieser Positionen wird unter Berücksichtigung der Kompetenzen von Patrick, Dominic und möglichen neuen Führungskräften festgelegt. Die Rollen von CEO und COO werden im Rahmen der Unternehmensgründung evaluiert.
### 2. Vergütungsstruktur
- **Führungsebene**: Marktübliche Vergütung zwischen CHF 150.000-180.000/Jahr je nach Position
- **Aktienoptionen**: Zusätzliche Aktienoptionen bei Erreichen definierter Unternehmensziele
## Meilensteine und Finanzielle Ziele
### Kritische Meilensteine (18 Monate)
| Zeitpunkt | Meilenstein | KPI |
|-----------|-------------|-----|
| Q3 2025 | Ausgründung & Finanzierung | Abschluss der Seed-Runde |
| Q4 2025 | Markteinführung | 3-5 Pilotprojekte |
| Q1 2026 | Produktvalidierung | 10+ zahlende Kunden |
| Q2 2026 | Skalierung | 15+ zahlende Kunden |
| Q4 2026 | Vorbereitung Serie A | 25+ Kunden, ARR: CHF 0,8 Mio. |
### Umsatz- und Bewertungsprognose
| Jahr | Kunden | ARR (CHF) | Valuation (CHF) | Multiplikator |
|------|--------|-----------|-----------------|---------------|
| Ende 2025 | 5-8 | 0,2 Mio. | 1,8 Mio. | 9x ARR |
| Ende 2026 | 25-30 | 0,8 Mio. | 5,6 Mio. | 7x ARR |
| Ende 2027 | 80-90 | 2,5 Mio. | 15 Mio. | 6x ARR |
| Ende 2028 | 150+ | 4,5 Mio. | 27 Mio. | 6x ARR |
## Liquiditätsoptionen
### Mittelfristige Optionen (2-3 Jahre)
1. **Serie A Finanzierung** (Ende 2026):
- Teilweise Liquidität für Gründer und ValueOn AG (10-15% ihrer Anteile)
- Zu erwartender Wert: CHF 5-6 Mio.
2. **Strategische Partnerschaft**:
- Investment durch strategischen Partner mit teilweisem Aktienrückkauf
- Potenzielle Partner: Enterprise Software-Anbieter, KI-Plattform-Betreiber
### Langfristige Optionen (4-7 Jahre)
1. **Komplette Übernahme**:
- Erwarteter Exit-Wert 2028: CHF 25-30 Mio.
- Vollständige Liquidität für alle Anteilseigner
2. **IPO-Vorbereitung**:
- Bei Erreichen von CHF 10+ Mio. ARR
- Potenzielle Bewertung: CHF 50-70 Mio.
## Nächste Schritte im Ausgründungsprozess
1. **Rechtliche Strukturierung**:
- Gründung der PowerOn AG
- Übertragungsvereinbarungen für geistiges Eigentum
- Aktionärsvereinbarungen
2. **Finanzierung**:
- Erstellung eines detaillierten Businessplans
- Vorbereitung des Investor Pitch Decks
- Ansprache potenzieller Investoren
3. **Organisationsaufbau**:
- Definition der Führungsstruktur und Schlüsselpositionen
- Rekrutierung des Kernteams
- Aufbau der operativen Prozesse
4. **Markteinführungsstrategie**:
- Festlegung der Go-to-Market Strategie
- Identifikation von Pilotprojekten
- Vorbereitung der Vertriebsunterlagen

View file

@ -1,6 +0,0 @@
id,name,email,phone,address,iban,credit_card,ahv_number
1,Max Mustermann,max.mustermann@beispiel.de,+49 30 12345678,Musterstraße 123 12345 Berlin,DE89 3704 0044 0532 0130 00,4532 1234 5678 9012,
2,Peter Schmid,peter.schmid@beispiel.ch,+41 44 123 45 67,Bahnhofstrasse 1 8001 Zürich,CH93 0076 7000 E529 3557 7,4532 1234 5678 9013,756.1234.5678.90
3,Marie Dupont,marie.dupont@exemple.fr,+33 1 23 45 67 89,123 Rue de Paris 75001 Paris,FR76 3000 6000 0112 3456 7890 189,4532 1234 5678 9014,
4,Marco Rossi,marco.rossi@esempio.it,+39 02 1234 5678,Via Roma 123 20100 Milano,IT60 X054 2811 1010 0000 0123 456,4532 1234 5678 9015,
5,John Smith,john.smith@example.com,+44 20 1234 5678,123 High Street London SW1A 1AA,GB29 NWBK 6016 1331 9268 19,4532 1234 5678 9016,
1 id name email phone address iban credit_card ahv_number
2 1 Max Mustermann max.mustermann@beispiel.de +49 30 12345678 Musterstraße 123 12345 Berlin DE89 3704 0044 0532 0130 00 4532 1234 5678 9012
3 2 Peter Schmid peter.schmid@beispiel.ch +41 44 123 45 67 Bahnhofstrasse 1 8001 Zürich CH93 0076 7000 E529 3557 7 4532 1234 5678 9013 756.1234.5678.90
4 3 Marie Dupont marie.dupont@exemple.fr +33 1 23 45 67 89 123 Rue de Paris 75001 Paris FR76 3000 6000 0112 3456 7890 189 4532 1234 5678 9014
5 4 Marco Rossi marco.rossi@esempio.it +39 02 1234 5678 Via Roma 123 20100 Milano IT60 X054 2811 1010 0000 0123 456 4532 1234 5678 9015
6 5 John Smith john.smith@example.com +44 20 1234 5678 123 High Street London SW1A 1AA GB29 NWBK 6016 1331 9268 19 4532 1234 5678 9016

View file

@ -1,73 +0,0 @@
Lebenslauf: Lara Meier
Persönliche Daten:
Name: Lara Meier
Geboren: 15.03.1990
Adresse: Bahnhofstrasse 45, 8001 Zürich
Telefon: +41 44 123 45 67
E-Mail: lara.meier@example.ch
AHV-Nr.: 756.1234.5678.90
Steuernummer: 123.456.789
Berufserfahrung:
2020-2023: Senior Projektmanagerin
Firma: TechSolutions AG
Adresse: Industriestrasse 100, 8004 Zürich
UID: CHE-123.456.789
Telefon: +41 44 987 65 43
E-Mail: lara.meier@techsolutions.ch
In dieser Position leitete ich ein Team von 15 Mitarbeitern und verantwortete die Implementierung von Cloud-Lösungen für internationale Kunden. Meine Hauptaufgaben umfassten:
- Projektplanung und -steuerung mit einem Budget von CHF 2.5 Mio.
- Kundenbetreuung und Stakeholder-Management
- Teamführung und Personalentwicklung
- Qualitätssicherung und Risikomanagement
2015-2020: Projektmanagerin
Firma: Digital Systems GmbH
Adresse: Musterstrasse 123, 8002 Zürich
UID: CHE-987.654.321
Telefon: +41 44 456 78 90
E-Mail: l.meier@digitalsystems.ch
Als Projektmanagerin verantwortete ich die erfolgreiche Durchführung von Digitalisierungsprojekten. Meine Leistungen:
- Implementierung von ERP-Systemen
- Optimierung von Geschäftsprozessen
- Schulung von Endbenutzern
- Erstellung von Projektdokumentation
Ausbildung:
2010-2015: ETH Zürich
Studiengang: Informatik
Matrikelnummer: 12-345-678
Abschluss: Master of Science in Computer Science
Thesis: "Künstliche Intelligenz in der Prozessautomatisierung"
2005-2010: Kantonsschule Zürich
Abschluss: Eidgenössische Maturität
Schwerpunkt: Mathematik und Naturwissenschaften
Sprachen:
Deutsch (Muttersprache)
Englisch (C2)
Französisch (B2)
Italienisch (B1)
Zertifizierungen:
PMP (Project Management Professional)
ITIL v4 Foundation
AWS Certified Solutions Architect
Scrum Master (PSM I)
Referenzen:
Prof. Dr. Hans Müller
ETH Zürich
Department of Computer Science
Telefon: +41 44 123 45 68
E-Mail: hans.mueller@ethz.ch
Dr. Sarah Weber
TechSolutions AG
CTO
Telefon: +41 44 987 65 44
E-Mail: sarah.weber@techsolutions.ch

View file

@ -1,6 +0,0 @@
employee_id,first_name,last_name,email,phone,department,office_address,uid_number,bank_account
E001,Hans,Müller,hans.mueller@firma.de,+49 89 12345678,IT,Hauptstraße 1 80331 München,,DE89 3704 0044 0532 0130 00
E002,Thomas,Weber,thomas.weber@firma.ch,+41 44 234 56 78,HR,Bahnhofstrasse 2 8001 Zürich,CHE-123.456.789,01-234567-8
E003,Sophie,Martin,sophie.martin@entreprise.fr,+33 1 34 56 78 90,Finance,15 Avenue des Champs-Élysées 75008 Paris,,FR76 3000 6000 0112 3456 7890 189
E004,Luca,Ferrari,luca.ferrari@azienda.it,+39 02 2345 6789,Marketing,Via Monte Napoleone 8 20121 Milano,,IT60 X054 2811 1010 0000 0123 456
E005,Emma,Wilson,emma.wilson@company.com,+44 20 2345 6789,Sales,25 Old Street London EC1V 9HL,,GB29 NWBK 6016 1331 9268 19
1 employee_id first_name last_name email phone department office_address uid_number bank_account
2 E001 Hans Müller hans.mueller@firma.de +49 89 12345678 IT Hauptstraße 1 80331 München DE89 3704 0044 0532 0130 00
3 E002 Thomas Weber thomas.weber@firma.ch +41 44 234 56 78 HR Bahnhofstrasse 2 8001 Zürich CHE-123.456.789 01-234567-8
4 E003 Sophie Martin sophie.martin@entreprise.fr +33 1 34 56 78 90 Finance 15 Avenue des Champs-Élysées 75008 Paris FR76 3000 6000 0112 3456 7890 189
5 E004 Luca Ferrari luca.ferrari@azienda.it +39 02 2345 6789 Marketing Via Monte Napoleone 8 20121 Milano IT60 X054 2811 1010 0000 0123 456
6 E005 Emma Wilson emma.wilson@company.com +44 20 2345 6789 Sales 25 Old Street London EC1V 9HL GB29 NWBK 6016 1331 9268 19

View file

@ -1,12 +0,0 @@
Dear Mr. Smith,
Thank you for your email to contact@example.com.
Your contact details have been recorded:
Phone: +44 20 1234 5678
Address: 123 High Street, London SW1A 1AA
Your IBAN: GB29 NWBK 6016 1331 9268 19
Credit Card: 4532 1234 5678 9012
Best regards,
John Doe

View file

@ -1,54 +0,0 @@
{
"customer": {
"personal_info": {
"name": "Max Mustermann",
"email": "max.mustermann@example.com",
"phone": "+49 123 4567890",
"address": {
"street": "Hauptstraße 123",
"city": "München",
"zip": "80331",
"country": "Deutschland"
}
},
"order_history": [
{
"order_id": "ORD-2024-001",
"date": "2024-03-15",
"items": [
{
"product": "Laptop",
"price": 1299.99,
"payment": {
"method": "credit_card",
"card_number": "4111 1111 1111 1111",
"iban": "DE89 3704 0044 0532 0130 00"
}
}
]
}
],
"preferences": {
"language": "de",
"newsletter": true,
"marketing_consent": {
"email": "max.mustermann@example.com",
"phone": "+49 123 4567890"
}
}
},
"company": {
"name": "Tech Solutions GmbH",
"contact": {
"manager": "Dr. Anna Schmidt",
"email": "anna.schmidt@techsolutions.de",
"phone": "+49 89 12345678",
"address": {
"street": "Technologiestraße 45",
"city": "Berlin",
"zip": "10115",
"country": "Deutschland"
}
}
}
}

View file

@ -1,79 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<company_data>
<employees>
<employee id="EMP001">
<personal_info>
<name>Dr. Thomas Weber</name>
<email>thomas.weber@company.de</email>
<phone>+49 30 12345678</phone>
<address>
<street>Musterstraße 42</street>
<city>Hamburg</city>
<zip>20095</zip>
<country>Deutschland</country>
</address>
</personal_info>
<employment>
<department>IT</department>
<position>Senior Developer</position>
<salary currency="EUR">75000</salary>
<bank_details>
<iban>DE89 3704 0044 0532 0130 00</iban>
<bic>COBADEFFXXX</bic>
</bank_details>
</employment>
</employee>
<employee id="EMP002">
<personal_info>
<name>Maria Schmidt</name>
<email>maria.schmidt@company.de</email>
<phone>+49 40 98765432</phone>
<address>
<street>Bahnhofstraße 15</street>
<city>Frankfurt</city>
<zip>60329</zip>
<country>Deutschland</country>
</address>
</personal_info>
<employment>
<department>HR</department>
<position>HR Manager</position>
<salary currency="EUR">65000</salary>
<bank_details>
<iban>DE27 3704 0044 0532 0130 01</iban>
<bic>COBADEFFXXX</bic>
</bank_details>
</employment>
</employee>
</employees>
<projects>
<project id="PRJ001">
<name>Digital Transformation</name>
<manager>Dr. Thomas Weber</manager>
<budget currency="EUR">150000</budget>
<team_members>
<member email="thomas.weber@company.de">Dr. Thomas Weber</member>
<member email="maria.schmidt@company.de">Maria Schmidt</member>
</team_members>
</project>
</projects>
<company_info>
<name>Tech Innovations GmbH</name>
<address>
<street>Innovationsstraße 1</street>
<city>München</city>
<zip>80331</zip>
<country>Deutschland</country>
</address>
<contact>
<email>info@techinnovations.de</email>
<phone>+49 89 12345678</phone>
<fax>+49 89 12345679</fax>
</contact>
<banking>
<iban>DE89 3704 0044 0532 0130 02</iban>
<bic>COBADEFFXXX</bic>
<tax_id>DE123456789</tax_id>
</banking>
</company_info>
</company_data>

View file

@ -1,12 +0,0 @@
Bonjour Madame Dupont,
Merci pour votre email à contact@exemple.fr.
Vos coordonnées ont été enregistrées:
Téléphone: +33 1 23 45 67 89
Adresse: 123 Rue de Paris, 75001 Paris
Votre IBAN: FR76 3000 6000 0112 3456 7890 189
Carte de crédit: 4532 1234 5678 9012
Cordialement,
Jean Martin

View file

@ -1,12 +0,0 @@
Sehr geehrte Frau Müller,
vielen Dank für Ihre E-Mail an max.mustermann@beispiel.de.
Ihre Telefonnummer 030-12345678 wurde in unserem System hinterlegt.
Die Rechnung wird an folgende Adresse gesendet:
Musterstraße 123, 12345 Berlin
Ihre IBAN: DE89 3704 0044 0532 0130 00
Kreditkarte: 4532 1234 5678 9012
Mit freundlichen Grüßen
Max Mustermann

View file

@ -1,56 +0,0 @@
Geschäftsstrategie 2024-2026
TechSolutions AG
Unternehmensdaten:
Firmenname: TechSolutions AG
UID: CHE-123.456.789
Adresse: Industriestrasse 100, 8004 Zürich
Telefon: +41 44 987 65 43
E-Mail: info@techsolutions.ch
Website: www.techsolutions.ch
Geschäftsführung:
CEO: Dr. Thomas Weber
E-Mail: thomas.weber@techsolutions.ch
Telefon: +41 44 987 65 44
CTO: Dr. Sarah Müller
E-Mail: sarah.mueller@techsolutions.ch
Telefon: +41 44 987 65 45
Finanzdaten:
Bank: UBS AG
IBAN: CH93 0076 7000 E529 0767 9
BIC: UBSWCHZH80A
Strategische Ziele:
1. Marktpositionierung
- Marktanteil in der Schweiz auf 25% steigern
- Expansion in DACH-Region
- Neue Zielgruppe: KMU
2. Produktentwicklung
- Cloud-Lösung für KMU
- KI-basierte Automatisierung
- Mobile App für Kunden
3. Personalentwicklung
- Team auf 50 Mitarbeiter ausbauen
- Neue Standorte: Bern und Genf
- Fokus auf Diversity & Inclusion
4. Nachhaltigkeit
- CO2-Neutralität bis 2025
- Green IT Zertifizierung
- Nachhaltige Lieferkette
Budgetplanung:
2024: CHF 5.2 Mio.
2025: CHF 6.8 Mio.
2026: CHF 8.5 Mio.
Risikoanalyse:
- Marktrisiken
- Technologische Risiken
- Personelle Risiken
- Finanzielle Risiken

Some files were not shown because too many files have changed in this diff Show more