feat(sync): Enhance Akte Sync with RAGflow support and improve error handling

This commit is contained in:
bsiggel
2026-03-26 23:09:42 +00:00
parent 1cd8de8574
commit 9bd62fc5ab

View File

@@ -4,7 +4,9 @@ Akte Sync - Event Handler
Unified sync for one CAkten entity across all configured backends: Unified sync for one CAkten entity across all configured backends:
- Advoware (3-way merge: Windows ↔ EspoCRM ↔ History) - Advoware (3-way merge: Windows ↔ EspoCRM ↔ History)
- xAI (Blake3 hash-based upload to Collection) - xAI (Blake3 hash-based upload to Collection)
- RAGflow (Dataset-based upload with laws chunk_method)
AI provider is selected via CAkten.aiProvider ('xai' or 'ragflow').
Both run in the same event to keep CDokumente perfectly in sync. Both run in the same event to keep CDokumente perfectly in sync.
Trigger: akte.sync { akte_id, aktennummer } Trigger: akte.sync { akte_id, aktennummer }
@@ -15,6 +17,8 @@ Enqueues:
- document.generate_preview (after CREATE / UPDATE_ESPO) - document.generate_preview (after CREATE / UPDATE_ESPO)
""" """
import traceback
import time
from typing import Dict, Any from typing import Dict, Any
from datetime import datetime from datetime import datetime
from motia import FlowContext, queue from motia import FlowContext, queue
@@ -22,7 +26,7 @@ from motia import FlowContext, queue
config = { config = {
"name": "Akte Sync - Event Handler", "name": "Akte Sync - Event Handler",
"description": "Unified sync for one Akte: Advoware 3-way merge + xAI upload", "description": "Unified sync for one Akte: Advoware 3-way merge + AI upload (xAI or RAGflow)",
"flows": ["akte-sync"], "flows": ["akte-sync"],
"triggers": [queue("akte.sync")], "triggers": [queue("akte.sync")],
"enqueues": ["document.generate_preview"], "enqueues": ["document.generate_preview"],
@@ -54,7 +58,7 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext) -> None:
return return
lock_key = f"akte_sync:{akte_id}" lock_key = f"akte_sync:{akte_id}"
lock_acquired = redis_client.set(lock_key, datetime.now().isoformat(), nx=True, ex=600) lock_acquired = redis_client.set(lock_key, datetime.now().isoformat(), nx=True, ex=1800) # 30 min
if not lock_acquired: if not lock_acquired:
ctx.logger.warn(f"⏸️ Lock busy for Akte {akte_id} requeueing") ctx.logger.warn(f"⏸️ Lock busy for Akte {akte_id} requeueing")
raise RuntimeError(f"Lock busy for akte_id={akte_id}") raise RuntimeError(f"Lock busy for akte_id={akte_id}")
@@ -104,13 +108,21 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext) -> None:
advoware_results = None advoware_results = None
if advoware_enabled: if advoware_enabled:
advoware_results = await _run_advoware_sync(akte, aktennummer, akte_id, espocrm, ctx, espo_docs) advoware_results = await _run_advoware_sync(akte, aktennummer, akte_id, espocrm, ctx, espo_docs)
# Re-fetch docs after Advoware sync newly created docs must be visible to AI sync
if ai_enabled and advoware_results and advoware_results.get('created', 0) > 0:
ctx.logger.info(
f" 🔄 Re-fetching docs after Advoware sync "
f"({advoware_results['created']} new doc(s) created)"
)
espo_docs = await espocrm.list_related_all('CAkten', akte_id, 'dokumentes')
# ── AI SYNC (xAI or RAGflow) ───────────────────────────────── # ── AI SYNC (xAI or RAGflow) ─────────────────────────────────
ai_had_failures = False
if ai_enabled: if ai_enabled:
if ai_provider.lower() == 'ragflow': if ai_provider.lower() == 'ragflow':
await _run_ragflow_sync(akte, akte_id, espocrm, ctx, espo_docs) ai_had_failures = await _run_ragflow_sync(akte, akte_id, espocrm, ctx, espo_docs)
else: else:
await _run_xai_sync(akte, akte_id, espocrm, ctx, espo_docs) ai_had_failures = await _run_xai_sync(akte, akte_id, espocrm, ctx, espo_docs)
# ── Final Status ─────────────────────────────────────────────────── # ── Final Status ───────────────────────────────────────────────────
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
@@ -123,7 +135,7 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext) -> None:
final_update['aktivierungsstatus'] = 'active' final_update['aktivierungsstatus'] = 'active'
ctx.logger.info("🔄 aktivierungsstatus: import → active") ctx.logger.info("🔄 aktivierungsstatus: import → active")
if ai_enabled: if ai_enabled:
final_update['aiSyncStatus'] = 'synced' final_update['aiSyncStatus'] = 'failed' if ai_had_failures else 'synced'
final_update['aiLastSync'] = now final_update['aiLastSync'] = now
# 'new' = Dataset/Collection erstmalig angelegt → auf 'aktiv' setzen # 'new' = Dataset/Collection erstmalig angelegt → auf 'aktiv' setzen
if ai_aktivierungsstatus == 'new': if ai_aktivierungsstatus == 'new':
@@ -143,11 +155,9 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext) -> None:
except Exception as e: except Exception as e:
ctx.logger.error(f"❌ Sync failed: {e}") ctx.logger.error(f"❌ Sync failed: {e}")
import traceback
ctx.logger.error(traceback.format_exc()) ctx.logger.error(traceback.format_exc())
# Requeue Advoware aktennummer for retry (Motia retries the akte.sync event itself) # Requeue Advoware aktennummer for retry (Motia retries the akte.sync event itself)
import time
if aktennummer: if aktennummer:
redis_client.zadd("advoware:pending_aktennummern", {aktennummer: time.time()}) redis_client.zadd("advoware:pending_aktennummern", {aktennummer: time.time()})
@@ -393,7 +403,7 @@ async def _run_xai_sync(
espocrm, espocrm,
ctx: FlowContext, ctx: FlowContext,
docs: list, docs: list,
) -> None: ) -> bool:
from services.xai_service import XAIService from services.xai_service import XAIService
from services.xai_upload_utils import XAIUploadUtils from services.xai_upload_utils import XAIUploadUtils
@@ -418,7 +428,7 @@ async def _run_xai_sync(
if not collection_id: if not collection_id:
ctx.logger.error("❌ xAI Collection konnte nicht erstellt werden Sync abgebrochen") ctx.logger.error("❌ xAI Collection konnte nicht erstellt werden Sync abgebrochen")
await espocrm.update_entity('CAkten', akte_id, {'aiSyncStatus': 'failed'}) await espocrm.update_entity('CAkten', akte_id, {'aiSyncStatus': 'failed'})
return return True # had failures
ctx.logger.info(f" ✅ Collection erstellt: {collection_id}") ctx.logger.info(f" ✅ Collection erstellt: {collection_id}")
# aiAktivierungsstatus → 'aktiv' wird in handler final_update gesetzt # aiAktivierungsstatus → 'aktiv' wird in handler final_update gesetzt
else: else:
@@ -428,7 +438,7 @@ async def _run_xai_sync(
f"xAI Sync abgebrochen. Bitte Collection-ID in EspoCRM eintragen." f"xAI Sync abgebrochen. Bitte Collection-ID in EspoCRM eintragen."
) )
await espocrm.update_entity('CAkten', akte_id, {'aiSyncStatus': 'failed'}) await espocrm.update_entity('CAkten', akte_id, {'aiSyncStatus': 'failed'})
return return True # had failures
else: else:
# Collection-ID vorhanden → verifizieren ob sie noch in xAI existiert # Collection-ID vorhanden → verifizieren ob sie noch in xAI existiert
try: try:
@@ -436,12 +446,12 @@ async def _run_xai_sync(
if not col: if not col:
ctx.logger.error(f"❌ Collection {collection_id} existiert nicht mehr in xAI Sync abgebrochen") ctx.logger.error(f"❌ Collection {collection_id} existiert nicht mehr in xAI Sync abgebrochen")
await espocrm.update_entity('CAkten', akte_id, {'aiSyncStatus': 'failed'}) await espocrm.update_entity('CAkten', akte_id, {'aiSyncStatus': 'failed'})
return return True # had failures
ctx.logger.info(f" ✅ Collection verifiziert: {collection_id}") ctx.logger.info(f" ✅ Collection verifiziert: {collection_id}")
except Exception as e: except Exception as e:
ctx.logger.error(f"❌ Collection-Verifizierung fehlgeschlagen: {e} Sync abgebrochen") ctx.logger.error(f"❌ Collection-Verifizierung fehlgeschlagen: {e} Sync abgebrochen")
await espocrm.update_entity('CAkten', akte_id, {'aiSyncStatus': 'failed'}) await espocrm.update_entity('CAkten', akte_id, {'aiSyncStatus': 'failed'})
return return True # had failures
ctx.logger.info(f" Documents to check: {len(docs)}") ctx.logger.info(f" Documents to check: {len(docs)}")
@@ -485,6 +495,7 @@ async def _run_xai_sync(
ctx.logger.info(f" ✅ Synced : {synced}") ctx.logger.info(f" ✅ Synced : {synced}")
ctx.logger.info(f" ⏭️ Skipped : {skipped}") ctx.logger.info(f" ⏭️ Skipped : {skipped}")
ctx.logger.info(f" ❌ Failed : {failed}") ctx.logger.info(f" ❌ Failed : {failed}")
return failed > 0
finally: finally:
await xai.close() await xai.close()
@@ -500,7 +511,7 @@ async def _run_ragflow_sync(
espocrm, espocrm,
ctx: FlowContext, ctx: FlowContext,
docs: list, docs: list,
) -> None: ) -> bool:
from services.ragflow_service import RAGFlowService from services.ragflow_service import RAGFlowService
from urllib.parse import unquote from urllib.parse import unquote
import mimetypes import mimetypes
@@ -512,192 +523,200 @@ async def _run_ragflow_sync(
ctx.logger.info("🧠 RAGflow SYNC") ctx.logger.info("🧠 RAGflow SYNC")
ctx.logger.info("" * 60) ctx.logger.info("" * 60)
ai_aktivierungsstatus = str(akte.get('aiAktivierungsstatus') or '').lower()
dataset_id = akte.get('aiCollectionId')
# ── Ensure dataset exists ─────────────────────────────────────────────
if not dataset_id:
if ai_aktivierungsstatus == 'new':
akte_name = akte.get('name') or f"Akte {akte.get('aktennummer', akte_id)}"
ctx.logger.info(f" Status 'new' → Erstelle neues RAGflow Dataset für '{akte_name}'...")
dataset_info = await ragflow.ensure_dataset(akte_name)
if not dataset_info or not dataset_info.get('id'):
ctx.logger.error("❌ RAGflow Dataset konnte nicht erstellt werden Sync abgebrochen")
await espocrm.update_entity('CAkten', akte_id, {'aiSyncStatus': 'failed'})
return
dataset_id = dataset_info['id']
ctx.logger.info(f" ✅ Dataset erstellt: {dataset_id}")
await espocrm.update_entity('CAkten', akte_id, {'aiCollectionId': dataset_id})
else:
ctx.logger.error(
f"❌ aiAktivierungsstatus='{ai_aktivierungsstatus}' aber keine aiCollectionId "
f"RAGflow Sync abgebrochen. Bitte Dataset-ID in EspoCRM eintragen."
)
await espocrm.update_entity('CAkten', akte_id, {'aiSyncStatus': 'failed'})
return
ctx.logger.info(f" Dataset-ID : {dataset_id}")
ctx.logger.info(f" EspoCRM docs: {len(docs)}")
# ── RAGflow-Bestand abrufen (source of truth) ─────────────────────────
# Lookup: espocrm_id → ragflow_doc (nur Docs die mit espocrm_id getaggt sind)
ragflow_by_espocrm_id: Dict[str, Any] = {}
try: try:
ragflow_docs = await ragflow.list_documents(dataset_id) ai_aktivierungsstatus = str(akte.get('aiAktivierungsstatus') or '').lower()
ctx.logger.info(f" RAGflow docs: {len(ragflow_docs)}") dataset_id = akte.get('aiCollectionId')
# ── Ensure dataset exists ─────────────────────────────────────────────
if not dataset_id:
if ai_aktivierungsstatus == 'new':
akte_name = akte.get('name') or f"Akte {akte.get('aktennummer', akte_id)}"
ctx.logger.info(f" Status 'new' → Erstelle neues RAGflow Dataset für '{akte_name}'...")
dataset_info = await ragflow.ensure_dataset(akte_name)
if not dataset_info or not dataset_info.get('id'):
ctx.logger.error("❌ RAGflow Dataset konnte nicht erstellt werden Sync abgebrochen")
await espocrm.update_entity('CAkten', akte_id, {'aiSyncStatus': 'failed'})
return True # had failures
dataset_id = dataset_info['id']
ctx.logger.info(f" ✅ Dataset erstellt: {dataset_id}")
await espocrm.update_entity('CAkten', akte_id, {'aiCollectionId': dataset_id})
else:
ctx.logger.error(
f"❌ aiAktivierungsstatus='{ai_aktivierungsstatus}' aber keine aiCollectionId "
f"RAGflow Sync abgebrochen. Bitte Dataset-ID in EspoCRM eintragen."
)
await espocrm.update_entity('CAkten', akte_id, {'aiSyncStatus': 'failed'})
return True # had failures
ctx.logger.info(f" Dataset-ID : {dataset_id}")
ctx.logger.info(f" EspoCRM docs: {len(docs)}")
# ── RAGflow-Bestand abrufen (source of truth) ─────────────────────────
ragflow_by_espocrm_id: Dict[str, Any] = {}
try:
ragflow_docs = await ragflow.list_documents(dataset_id)
ctx.logger.info(f" RAGflow docs: {len(ragflow_docs)}")
for rd in ragflow_docs:
eid = rd.get('espocrm_id')
if eid:
ragflow_by_espocrm_id[eid] = rd
except Exception as e:
ctx.logger.error(f"❌ RAGflow Dokumentenliste nicht abrufbar: {e}")
await espocrm.update_entity('CAkten', akte_id, {'aiSyncStatus': 'failed'})
return True # had failures
# ── Orphan-Cleanup: RAGflow-Docs die kein EspoCRM-Äquivalent mehr haben ──
espocrm_ids_set = {d['id'] for d in docs}
for rd in ragflow_docs: for rd in ragflow_docs:
eid = rd.get('espocrm_id') eid = rd.get('espocrm_id')
if eid: if eid and eid not in espocrm_ids_set:
ragflow_by_espocrm_id[eid] = rd try:
except Exception as e: await ragflow.remove_document(dataset_id, rd['id'])
ctx.logger.error(f"❌ RAGflow Dokumentenliste nicht abrufbar: {e}") ctx.logger.info(f" 🗑️ Orphan gelöscht: {rd.get('name', rd['id'])} (espocrm_id={eid})")
await espocrm.update_entity('CAkten', akte_id, {'aiSyncStatus': 'failed'}) except Exception as e:
return ctx.logger.warn(f" ⚠️ Orphan-Delete fehlgeschlagen: {e}")
# ── Orphan-Cleanup: RAGflow-Docs die kein EspoCRM-Äquivalent mehr haben ── synced = 0
espocrm_ids_set = {d['id'] for d in docs} skipped = 0
for rd in ragflow_docs: failed = 0
eid = rd.get('espocrm_id')
if eid and eid not in espocrm_ids_set:
try:
await ragflow.remove_document(dataset_id, rd['id'])
ctx.logger.info(f" 🗑️ Orphan gelöscht: {rd.get('name', rd['id'])} (espocrm_id={eid})")
except Exception as e:
ctx.logger.warn(f" ⚠️ Orphan-Delete fehlgeschlagen: {e}")
synced = 0 for doc in docs:
skipped = 0 doc_id = doc['id']
failed = 0 doc_name = doc.get('name', doc_id)
blake3_hash = doc.get('blake3hash') or ''
for doc in docs: # Was ist aktuell in RAGflow für dieses Dokument?
doc_id = doc['id'] ragflow_doc = ragflow_by_espocrm_id.get(doc_id)
doc_name = doc.get('name', doc_id) ragflow_doc_id = ragflow_doc['id'] if ragflow_doc else None
blake3_hash = doc.get('blake3hash') or '' ragflow_blake3 = ragflow_doc.get('blake3_hash', '') if ragflow_doc else ''
ragflow_meta = ragflow_doc.get('meta_fields', {}) if ragflow_doc else {}
# Was ist aktuell in RAGflow für dieses Dokument? # Aktuelle Metadaten aus EspoCRM
ragflow_doc = ragflow_by_espocrm_id.get(doc_id) current_description = str(doc.get('beschreibung') or '')
ragflow_doc_id = ragflow_doc['id'] if ragflow_doc else None current_advo_art = str(doc.get('advowareArt') or '')
ragflow_blake3 = ragflow_doc.get('blake3_hash', '') if ragflow_doc else '' current_advo_bemerk = str(doc.get('advowareBemerkung') or '')
ragflow_meta = ragflow_doc.get('meta_fields', {}) if ragflow_doc else {}
# Aktuelle Metadaten aus EspoCRM content_changed = blake3_hash != ragflow_blake3
current_description = str(doc.get('beschreibung') or '') meta_changed = (
current_advo_art = str(doc.get('advowareArt') or '') ragflow_meta.get('description', '') != current_description or
current_advo_bemerk = str(doc.get('advowareBemerkung') or '') ragflow_meta.get('advoware_art', '') != current_advo_art or
ragflow_meta.get('advoware_bemerkung', '') != current_advo_bemerk
content_changed = blake3_hash != ragflow_blake3
meta_changed = (
ragflow_meta.get('description', '') != current_description or
ragflow_meta.get('advoware_art', '') != current_advo_art or
ragflow_meta.get('advoware_bemerkung', '') != current_advo_bemerk
)
ctx.logger.info(f" 📄 {doc_name}")
ctx.logger.info(
f" in_ragflow={bool(ragflow_doc_id)}, "
f"content_changed={content_changed}, meta_changed={meta_changed}"
)
if ragflow_doc_id:
ctx.logger.info(
f" ragflow_blake3={ragflow_blake3[:12] if ragflow_blake3 else 'N/A'}..., "
f"espo_blake3={blake3_hash[:12] if blake3_hash else 'N/A'}..."
) )
if not ragflow_doc_id and not content_changed and not meta_changed and not blake3_hash: ctx.logger.info(f" 📄 {doc_name}")
# Kein Attachment-Hash vorhanden und noch nie in RAGflow → unsupported ctx.logger.info(
ctx.logger.info(f" ⏭️ Kein Blake3-Hash übersprungen") f" in_ragflow={bool(ragflow_doc_id)}, "
skipped += 1 f"content_changed={content_changed}, meta_changed={meta_changed}"
continue )
if ragflow_doc_id:
attachment_id = doc.get('dokumentId') ctx.logger.info(
if not attachment_id: f" ragflow_blake3={ragflow_blake3[:12] if ragflow_blake3 else 'N/A'}..., "
ctx.logger.warn(f" ⚠️ Kein Attachment (dokumentId fehlt) unsupported") f"espo_blake3={blake3_hash[:12] if blake3_hash else 'N/A'}..."
await espocrm.update_entity('CDokumente', doc_id, {
'aiSyncStatus': 'unsupported',
'aiLastSync': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
})
skipped += 1
continue
filename = unquote(doc.get('dokumentName') or doc.get('name') or 'document.bin')
mime_type, _ = mimetypes.guess_type(filename)
if not mime_type:
mime_type = 'application/octet-stream'
try:
if ragflow_doc_id and not content_changed and meta_changed:
# ── Nur Metadaten aktualisieren ───────────────────────────
ctx.logger.info(f" 🔄 Metadata-Update für {ragflow_doc_id}")
await ragflow.update_document_meta(
dataset_id, ragflow_doc_id,
blake3_hash=blake3_hash,
description=current_description,
advoware_art=current_advo_art,
advoware_bemerkung=current_advo_bemerk,
) )
new_ragflow_id = ragflow_doc_id
elif ragflow_doc_id and not content_changed and not meta_changed: if not ragflow_doc_id and not blake3_hash:
# ── Vollständig unverändert → Skip ──────────────────────── ctx.logger.info(f" ⏭️ Kein Blake3-Hash übersprungen")
ctx.logger.info(f" ✅ Unverändert kein Re-Upload") skipped += 1
# Tracking-Felder in EspoCRM aktuell halten continue
attachment_id = doc.get('dokumentId')
if not attachment_id:
ctx.logger.warn(f" ⚠️ Kein Attachment (dokumentId fehlt) unsupported")
await espocrm.update_entity('CDokumente', doc_id, { await espocrm.update_entity('CDokumente', doc_id, {
'aiFileId': ragflow_doc_id, 'aiSyncStatus': 'unsupported',
'aiCollectionId': dataset_id, 'aiLastSync': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'aiSyncHash': blake3_hash,
'aiSyncStatus': 'synced',
}) })
skipped += 1 skipped += 1
continue continue
else: filename = unquote(doc.get('dokumentName') or doc.get('name') or 'document.bin')
# ── Upload (neu oder Inhalt geändert) ───────────────────── mime_type, _ = mimetypes.guess_type(filename)
if ragflow_doc_id and content_changed: if not mime_type:
ctx.logger.info(f" 🗑️ Inhalt geändert altes Dokument löschen: {ragflow_doc_id}") mime_type = 'application/octet-stream'
try:
await ragflow.remove_document(dataset_id, ragflow_doc_id)
except Exception:
pass
ctx.logger.info(f" 📥 Downloading {filename} ({attachment_id})…") try:
file_content = await espocrm.download_attachment(attachment_id) if ragflow_doc_id and not content_changed and meta_changed:
ctx.logger.info(f" Downloaded {len(file_content)} bytes") # ── Nur Metadaten aktualisieren ───────────────────────────
ctx.logger.info(f" 🔄 Metadata-Update für {ragflow_doc_id}")
await ragflow.update_document_meta(
dataset_id, ragflow_doc_id,
blake3_hash=blake3_hash,
description=current_description,
advoware_art=current_advo_art,
advoware_bemerkung=current_advo_bemerk,
)
new_ragflow_id = ragflow_doc_id
ctx.logger.info(f" 📤 Uploading '{filename}' ({mime_type})…") elif ragflow_doc_id and not content_changed and not meta_changed:
result = await ragflow.upload_document( # ── Vollständig unverändert → Skip ────────────────────────
dataset_id=dataset_id, ctx.logger.info(f" ✅ Unverändert kein Re-Upload")
file_content=file_content, await espocrm.update_entity('CDokumente', doc_id, {
filename=filename, 'aiFileId': ragflow_doc_id,
mime_type=mime_type, 'aiCollectionId': dataset_id,
blake3_hash=blake3_hash, 'aiSyncHash': blake3_hash,
espocrm_id=doc_id, 'aiSyncStatus': 'synced',
description=current_description, })
advoware_art=current_advo_art, skipped += 1
advoware_bemerkung=current_advo_bemerk, continue
)
if not result or not result.get('id'):
raise RuntimeError("upload_document gab kein Ergebnis zurück")
new_ragflow_id = result['id']
ctx.logger.info(f" ✅ RAGflow-ID: {new_ragflow_id}") else:
now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S') # ── Upload (neu oder Inhalt geändert) ─────────────────────
await espocrm.update_entity('CDokumente', doc_id, { if ragflow_doc_id and content_changed:
'aiFileId': new_ragflow_id, ctx.logger.info(f" 🗑️ Inhalt geändert altes Dokument löschen: {ragflow_doc_id}")
'aiCollectionId': dataset_id, try:
'aiSyncHash': blake3_hash, await ragflow.remove_document(dataset_id, ragflow_doc_id)
'aiSyncStatus': 'synced', except Exception:
'aiLastSync': now_str, pass
})
synced += 1
except Exception as e: ctx.logger.info(f" 📥 Downloading {filename} ({attachment_id})…")
ctx.logger.error(f" ❌ Fehlgeschlagen: {e}") file_content = await espocrm.download_attachment(attachment_id)
await espocrm.update_entity('CDokumente', doc_id, { ctx.logger.info(f" Downloaded {len(file_content)} bytes")
'aiSyncStatus': 'failed',
'aiLastSync': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
})
failed += 1
ctx.logger.info(f" ✅ Synced : {synced}") ctx.logger.info(f" 📤 Uploading '{filename}' ({mime_type})…")
ctx.logger.info(f" ⏭️ Skipped : {skipped}") result = await ragflow.upload_document(
ctx.logger.info(f" ❌ Failed : {failed}") dataset_id=dataset_id,
file_content=file_content,
filename=filename,
mime_type=mime_type,
blake3_hash=blake3_hash,
espocrm_id=doc_id,
description=current_description,
advoware_art=current_advo_art,
advoware_bemerkung=current_advo_bemerk,
)
if not result or not result.get('id'):
raise RuntimeError("upload_document gab kein Ergebnis zurück")
new_ragflow_id = result['id']
ctx.logger.info(f" ✅ RAGflow-ID: {new_ragflow_id}")
now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
await espocrm.update_entity('CDokumente', doc_id, {
'aiFileId': new_ragflow_id,
'aiCollectionId': dataset_id,
'aiSyncHash': blake3_hash,
'aiSyncStatus': 'synced',
'aiLastSync': now_str,
})
synced += 1
except Exception as e:
ctx.logger.error(f" ❌ Fehlgeschlagen: {e}")
await espocrm.update_entity('CDokumente', doc_id, {
'aiSyncStatus': 'failed',
'aiLastSync': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
})
failed += 1
ctx.logger.info(f" ✅ Synced : {synced}")
ctx.logger.info(f" ⏭️ Skipped : {skipped}")
ctx.logger.info(f" ❌ Failed : {failed}")
return failed > 0
except Exception as e:
ctx.logger.error(f"❌ RAGflow Sync unerwarteter Fehler: {e}")
ctx.logger.error(traceback.format_exc())
try:
await espocrm.update_entity('CAkten', akte_id, {'aiSyncStatus': 'failed'})
except Exception:
pass
return True # had failures