cleaned model space centralized
This commit is contained in:
parent
842fe46a87
commit
07a2d279df
107 changed files with 1651 additions and 7101 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
)
|
|
||||||
|
|
|
||||||
17
modules/datamodels/__init__.py
Normal file
17
modules/datamodels/__init__.py
Normal 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
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
||||||
262
modules/datamodels/datamodelChat.py
Normal file
262
modules/datamodels/datamodelChat.py
Normal 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"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
84
modules/datamodels/datamodelFiles.py
Normal file
84
modules/datamodels/datamodelFiles.py
Normal 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"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
56
modules/datamodels/datamodelNeutralizer.py
Normal file
56
modules/datamodels/datamodelNeutralizer.py
Normal 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"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
87
modules/datamodels/datamodelSecurity.py
Normal file
87
modules/datamodels/datamodelSecurity.py
Normal 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"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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: ...
|
||||||
|
|
||||||
|
|
||||||
154
modules/datamodels/datamodelUam.py
Normal file
154
modules/datamodels/datamodelUam.py
Normal 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"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
26
modules/datamodels/datamodelUtils.py
Normal file
26
modules/datamodels/datamodelUtils.py
Normal 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"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
43
modules/datamodels/datamodelVoice.py
Normal file
43
modules/datamodels/datamodelVoice.py
Normal 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"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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: ...
|
||||||
|
|
||||||
|
|
||||||
446
modules/datamodels/datamodelWorkflow.py
Normal file
446
modules/datamodels/datamodelWorkflow.py
Normal 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"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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__)
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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)}")
|
||||||
|
|
|
||||||
|
|
@ -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__)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
@ -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__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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__)
|
||||||
|
|
|
||||||
|
|
@ -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"}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
)
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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__)
|
||||||
|
|
|
||||||
|
|
@ -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__)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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__)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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__)
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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__)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
49
modules/services/serviceWeb/mainServiceWeb.py
Normal file
49
modules/services/serviceWeb/mainServiceWeb.py
Normal 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
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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__)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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'):
|
||||||
|
|
|
||||||
|
|
@ -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__)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
|
@ -1 +0,0 @@
|
||||||
# noqa
|
|
||||||
0
tests/fixtures/__init__.py
vendored
0
tests/fixtures/__init__.py
vendored
71
tests/fixtures/tavily_responses.py
vendored
71
tests/fixtures/tavily_responses.py
vendored
File diff suppressed because one or more lines are too long
|
|
@ -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}")
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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
|
|
||||||
|
File diff suppressed because it is too large
Load diff
|
|
@ -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)})
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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,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]
|
|
||||||
|
|
@ -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,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
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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,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
|
|
||||||
|
|
@ -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,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,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
|
|
||||||
|
|
@ -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,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
|
|
||||||
608
tests/test_neutralizer/testdata/Case.md
vendored
608
tests/test_neutralizer/testdata/Case.md
vendored
|
|
@ -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
|
|
||||||
|
|
@ -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,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
|
|
||||||
|
|
@ -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
|
|
||||||
|
12
tests/test_neutralizer/testdata/english.txt
vendored
12
tests/test_neutralizer/testdata/english.txt
vendored
|
|
@ -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
|
|
||||||
54
tests/test_neutralizer/testdata/example.json
vendored
54
tests/test_neutralizer/testdata/example.json
vendored
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
79
tests/test_neutralizer/testdata/example.xml
vendored
79
tests/test_neutralizer/testdata/example.xml
vendored
|
|
@ -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>
|
|
||||||
12
tests/test_neutralizer/testdata/french.txt
vendored
12
tests/test_neutralizer/testdata/french.txt
vendored
|
|
@ -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
|
|
||||||
12
tests/test_neutralizer/testdata/german.txt
vendored
12
tests/test_neutralizer/testdata/german.txt
vendored
|
|
@ -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
|
|
||||||
|
|
@ -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
Loading…
Reference in a new issue