- Zapier Document Refresh action (create) added

This commit is contained in:
Josako
2024-12-17 16:40:21 +01:00
parent 53c625599a
commit f7cd58ed2a
9 changed files with 381 additions and 194 deletions

View File

@@ -660,3 +660,48 @@ def json_to_pattern_list(json_content: str) -> list:
return patterns return patterns
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON format: {e}") raise ValueError(f"Invalid JSON format: {e}")
def normalize_json_field(value: str | dict | None, field_name: str = "JSON field") -> dict:
"""
Normalize a JSON field value to ensure it's a valid dictionary.
Args:
value: The input value which can be:
- None (will return empty dict)
- String (will be parsed as JSON)
- Dict (will be validated and returned)
field_name: Name of the field for error messages
Returns:
dict: The normalized JSON data as a Python dictionary
Raises:
ValueError: If the input string is not valid JSON or the input dict contains invalid types
"""
# Handle None case
if value is None:
return {}
# Handle dictionary case
if isinstance(value, dict):
try:
# Validate all values are JSON serializable
import json
json.dumps(value)
return value
except TypeError as e:
raise ValueError(f"{field_name} contains invalid types: {str(e)}")
# Handle string case
if isinstance(value, str):
if not value.strip():
return {}
try:
import json
return json.loads(value)
except json.JSONDecodeError as e:
raise ValueError(f"{field_name} contains invalid JSON: {str(e)}")
raise ValueError(f"{field_name} must be a string, dictionary, or None (got {type(value)})")

View File

@@ -11,6 +11,8 @@ from flask_security import current_user
import requests import requests
from urllib.parse import urlparse, unquote, urlunparse from urllib.parse import urlparse, unquote, urlunparse
import os import os
from .config_field_types import normalize_json_field
from .eveai_exceptions import (EveAIInvalidLanguageException, EveAIDoubleURLException, EveAIUnsupportedFileType, from .eveai_exceptions import (EveAIInvalidLanguageException, EveAIDoubleURLException, EveAIUnsupportedFileType,
EveAIInvalidCatalog, EveAIInvalidDocument, EveAIInvalidDocumentVersion, EveAIException) EveAIInvalidCatalog, EveAIInvalidDocument, EveAIInvalidDocumentVersion, EveAIException)
from ..models.user import Tenant from ..models.user import Tenant
@@ -88,10 +90,10 @@ def create_version_for_document(document, tenant_id, url, sub_file_type, langua
new_doc_vers.user_context = user_context new_doc_vers.user_context = user_context
if user_metadata != '' and user_metadata is not None: if user_metadata != '' and user_metadata is not None:
new_doc_vers.user_metadata = user_metadata new_doc_vers.user_metadata = normalize_json_field(user_metadata, "user_metadata")
if catalog_properties != '' and catalog_properties is not None: if catalog_properties != '' and catalog_properties is not None:
new_doc_vers.catalog_properties = catalog_properties new_doc_vers.catalog_properties = normalize_json_field(catalog_properties, "catalog_properties")
if sub_file_type != '': if sub_file_type != '':
new_doc_vers.sub_file_type = sub_file_type new_doc_vers.sub_file_type = sub_file_type
@@ -262,7 +264,8 @@ def edit_document_version(tenant_id, version_id, user_context, catalog_propertie
if not doc_vers: if not doc_vers:
raise EveAIInvalidDocumentVersion(tenant_id, version_id) raise EveAIInvalidDocumentVersion(tenant_id, version_id)
doc_vers.user_context = user_context doc_vers.user_context = user_context
doc_vers.catalog_properties = catalog_properties doc_vers.catalog_properties = normalize_json_field(catalog_properties, "catalog_properties")
update_logging_information(doc_vers, dt.now(tz.utc)) update_logging_information(doc_vers, dt.now(tz.utc))
try: try:
@@ -319,6 +322,56 @@ def refresh_document_with_info(doc_id, tenant_id, api_input):
return new_doc_vers, task.id return new_doc_vers, task.id
def refresh_document_with_content(doc_id: int, tenant_id: int, file_content: bytes, api_input: dict) -> tuple:
"""
Refresh document with new content
Args:
doc_id: Document ID
tenant_id: Tenant ID
file_content: New file content
api_input: Additional document information
Returns:
Tuple of (new_version, task_id)
"""
doc = Document.query.get(doc_id)
if not doc:
raise EveAIInvalidDocument(tenant_id, doc_id)
old_doc_vers = DocumentVersion.query.filter_by(doc_id=doc_id).order_by(desc(DocumentVersion.id)).first()
# Create new version with same file type as original
extension = old_doc_vers.file_type
new_doc_vers = create_version_for_document(
doc, tenant_id,
'', # No URL for content-based updates
old_doc_vers.sub_file_type,
api_input.get('language', old_doc_vers.language),
api_input.get('user_context', old_doc_vers.user_context),
api_input.get('user_metadata', old_doc_vers.user_metadata),
api_input.get('catalog_properties', old_doc_vers.catalog_properties),
)
try:
db.session.add(new_doc_vers)
db.session.commit()
except SQLAlchemyError as e:
db.session.rollback()
return None, str(e)
# Upload new content
upload_file_for_version(new_doc_vers, file_content, extension, tenant_id)
# Start embedding task
task = current_celery.send_task('create_embeddings', args=[tenant_id, new_doc_vers.id], queue='embeddings')
current_app.logger.info(f'Embedding creation started for document {doc_id} on version {new_doc_vers.id} '
f'with task id: {task.id}.')
return new_doc_vers, task.id
# Update the existing refresh_document function to use the new refresh_document_with_info # Update the existing refresh_document function to use the new refresh_document_with_info
def refresh_document(doc_id, tenant_id): def refresh_document(doc_id, tenant_id):
current_app.logger.info(f'Refreshing document {doc_id}') current_app.logger.info(f'Refreshing document {doc_id}')
@@ -388,6 +441,10 @@ def lookup_document(tenant_id: int, lookup_criteria: dict, metadata_type: str) -
for key, value in lookup_criteria.items(): for key, value in lookup_criteria.items():
query = query.filter(metadata_field[key].astext == str(value)) query = query.filter(metadata_field[key].astext == str(value))
# Log the final SQL query
current_app.logger.debug(
f"Final SQL query: {query.statement.compile(compile_kwargs={'literal_binds': True})}")
# Get first result # Get first result
result = query.first() result = query.first()
@@ -411,55 +468,3 @@ def lookup_document(tenant_id: int, lookup_criteria: dict, metadata_type: str) -
"Error during document lookup", "Error during document lookup",
status_code=500 status_code=500
) )
# Add to common/utils/document_utils.py
def refresh_document_with_content(doc_id: int, tenant_id: int, file_content: bytes, api_input: dict) -> tuple:
"""
Refresh document with new content
Args:
doc_id: Document ID
tenant_id: Tenant ID
file_content: New file content
api_input: Additional document information
Returns:
Tuple of (new_version, task_id)
"""
doc = Document.query.get(doc_id)
if not doc:
raise EveAIInvalidDocument(tenant_id, doc_id)
old_doc_vers = DocumentVersion.query.filter_by(doc_id=doc_id).order_by(desc(DocumentVersion.id)).first()
# Create new version with same file type as original
extension = old_doc_vers.file_type
new_doc_vers = create_version_for_document(
doc, tenant_id,
'', # No URL for content-based updates
old_doc_vers.sub_file_type,
api_input.get('language', old_doc_vers.language),
api_input.get('user_context', old_doc_vers.user_context),
api_input.get('user_metadata', old_doc_vers.user_metadata),
api_input.get('catalog_properties', old_doc_vers.catalog_properties),
)
try:
db.session.add(new_doc_vers)
db.session.commit()
except SQLAlchemyError as e:
db.session.rollback()
return None, str(e)
# Upload new content
upload_file_for_version(new_doc_vers, file_content, extension, tenant_id)
# Start embedding task
task = current_celery.send_task('create_embeddings', args=[tenant_id, new_doc_vers.id], queue='embeddings')
current_app.logger.info(f'Embedding creation started for document {doc_id} on version {new_doc_vers.id} '
f'with task id: {task.id}.')
return new_doc_vers, task.id

View File

@@ -1,6 +1,6 @@
import traceback import traceback
from flask import Flask, jsonify, request from flask import Flask, jsonify, request, redirect
from flask_jwt_extended import get_jwt_identity, verify_jwt_in_request from flask_jwt_extended import get_jwt_identity, verify_jwt_in_request
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
@@ -103,14 +103,19 @@ def create_app(config_file=None):
@app.route('/api/v1') @app.route('/api/v1')
def swagger(): def swagger():
return api_rest.render_doc() return redirect('/api/v1/')
return app return app
def register_extensions(app): def register_extensions(app):
db.init_app(app) db.init_app(app)
api_rest.init_app(app, title='EveAI API', version='1.0', description='EveAI API') api_rest.init_app(app,
title='EveAI API',
version='1.0',
description='EveAI API',
doc='/api/v1/',
prefix='/api/v1'),
jwt.init_app(app) jwt.init_app(app)
minio_client.init_app(app) minio_client.init_app(app)
simple_encryption.init_app(app) simple_encryption.init_app(app)

View File

@@ -1,14 +1,18 @@
import io import io
import json import json
from datetime import datetime from datetime import datetime
from typing import Tuple, Any
import pytz import pytz
import requests import requests
from flask import current_app, request from flask import current_app, request
from flask_restx import Namespace, Resource, fields, reqparse from flask_restx import Namespace, Resource, fields, reqparse
from flask_jwt_extended import jwt_required, get_jwt_identity from flask_jwt_extended import jwt_required, get_jwt_identity
from sqlalchemy import desc
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from common.models.document import DocumentVersion
from common.utils.document_utils import ( from common.utils.document_utils import (
create_document_stack, process_url, start_embedding_task, create_document_stack, process_url, start_embedding_task,
EveAIInvalidLanguageException, EveAIDoubleURLException, EveAIUnsupportedFileType, EveAIInvalidLanguageException, EveAIDoubleURLException, EveAIUnsupportedFileType,
@@ -37,7 +41,8 @@ document_ns = Namespace('documents', description='Document related operations')
# Define models for request parsing and response serialization # Define models for request parsing and response serialization
upload_parser = reqparse.RequestParser() upload_parser = reqparse.RequestParser()
upload_parser.add_argument('catalog_id', location='form', type=int, required=True, help='The catalog to add the file to') upload_parser.add_argument('catalog_id', location='form', type=int, required=True,
help='The catalog to add the file to')
upload_parser.add_argument('file', location='files', type=FileStorage, required=True, help='The file to upload') upload_parser.add_argument('file', location='files', type=FileStorage, required=True, help='The file to upload')
upload_parser.add_argument('name', location='form', type=str, required=False, help='Name of the document') upload_parser.add_argument('name', location='form', type=str, required=False, help='Name of the document')
upload_parser.add_argument('language', location='form', type=str, required=True, help='Language of the document') upload_parser.add_argument('language', location='form', type=str, required=True, help='Language of the document')
@@ -69,7 +74,11 @@ class AddDocument(Resource):
@document_ns.response(500, 'Internal Server Error') @document_ns.response(500, 'Internal Server Error')
def post(self): def post(self):
""" """
Add a new document by providing the content of a file (Multipart/form-data). Upload a new document to EveAI by directly providing the file content.
This endpoint accepts multipart/form-data with the file content and metadata. It processes
the file, creates a new document in the specified catalog, and initiates the embedding
process.
""" """
tenant_id = get_jwt_identity() tenant_id = get_jwt_identity()
current_app.logger.info(f'Adding document for tenant {tenant_id}') current_app.logger.info(f'Adding document for tenant {tenant_id}')
@@ -126,10 +135,11 @@ add_document_through_url = document_ns.model('AddDocumentThroughURL', {
'valid_from': fields.String(required=False, description='Valid from date for the document'), 'valid_from': fields.String(required=False, description='Valid from date for the document'),
'user_metadata': fields.String(required=False, description='User metadata for the document'), 'user_metadata': fields.String(required=False, description='User metadata for the document'),
'system_metadata': fields.String(required=False, description='System metadata for the document'), 'system_metadata': fields.String(required=False, description='System metadata for the document'),
'catalog_properties': fields.String(required=False, description='The catalog configuration to be passed along (JSON ' 'catalog_properties': fields.String(required=False,
'format). Validity is against catalog requirements ' description='The catalog configuration to be passed along (JSON '
'is not checked, and is the responsibility of the ' 'format). Validity is against catalog requirements '
'calling client.'), 'is not checked, and is the responsibility of the '
'calling client.'),
}) })
add_document_through_url_response = document_ns.model('AddDocumentThroughURLResponse', { add_document_through_url_response = document_ns.model('AddDocumentThroughURLResponse', {
@@ -139,6 +149,7 @@ add_document_through_url_response = document_ns.model('AddDocumentThroughURLResp
'task_id': fields.String(description='ID of the embedding task') 'task_id': fields.String(description='ID of the embedding task')
}) })
@document_ns.route('/add_document_through_url') @document_ns.route('/add_document_through_url')
class AddDocumentThroughURL(Resource): class AddDocumentThroughURL(Resource):
@jwt_required() @jwt_required()
@@ -150,8 +161,10 @@ class AddDocumentThroughURL(Resource):
@document_ns.response(500, 'Internal Server Error') @document_ns.response(500, 'Internal Server Error')
def post(self): def post(self):
""" """
Add a new document using a URL. The URL can be temporary, and will not be stored. Add a new document to EveAI using a temporary URL.
Mainly used for passing temporary URLs like used in e.g. Zapier
This endpoint is primarily used for integration with services that provide temporary URLs
(like Zapier). The URL content is downloaded and processed as a new document.
""" """
tenant_id = get_jwt_identity() tenant_id = get_jwt_identity()
current_app.logger.info(f'Adding document through url for tenant {tenant_id}') current_app.logger.info(f'Adding document through url for tenant {tenant_id}')
@@ -164,29 +177,19 @@ class AddDocumentThroughURL(Resource):
raise raise
try: try:
# Step 1: Download from stashed URL user_metadata = json.loads(args.get('user_metadata', '{}'))
stashed_url = args['temp_url'] actual_file_content, actual_file_content_type = download_file_content(args['temp_url'], user_metadata)
current_app.logger.info(f"Downloading stashed file from URL: {stashed_url}")
response = requests.get(stashed_url, stream=True)
response.raise_for_status()
hydration_url = response.text.strip()
current_app.logger.info(f"Downloading actual file from URL: {hydration_url}")
# Step 2: Download from hydration URL
actual_file_response = requests.get(hydration_url, stream=True)
actual_file_response.raise_for_status()
hydrated_file_content = actual_file_response.content
# Get filename from URL or use provided name # Get filename from URL or use provided name
filename = secure_filename(args.get('name')) filename = secure_filename(args.get('name'))
extension = filename.rsplit('.', 1)[1].lower() if '.' in filename else '' extension = filename.rsplit('.', 1)[1].lower() if '.' in filename else ''
# Create FileStorage object from downloaded content # Create FileStorage object from downloaded content
file_content = io.BytesIO(hydrated_file_content) file_content = io.BytesIO(actual_file_content)
file = FileStorage( file = FileStorage(
stream=file_content, stream=file_content,
filename=filename, filename=filename,
content_type=response.headers.get('content-type', 'application/octet-stream') content_type=actual_file_content_type
) )
current_app.logger.info(f"Successfully downloaded file: {filename}") current_app.logger.info(f"Successfully downloaded file: {filename}")
@@ -233,10 +236,11 @@ add_url_model = document_ns.model('AddURL', {
'valid_from': fields.String(required=False, description='Valid from date for the document'), 'valid_from': fields.String(required=False, description='Valid from date for the document'),
'user_metadata': fields.String(required=False, description='User metadata for the document'), 'user_metadata': fields.String(required=False, description='User metadata for the document'),
'system_metadata': fields.String(required=False, description='System metadata for the document'), 'system_metadata': fields.String(required=False, description='System metadata for the document'),
'catalog_properties': fields.String(required=False, description='The catalog configuration to be passed along (JSON ' 'catalog_properties': fields.String(required=False,
'format). Validity is against catalog requirements ' description='The catalog configuration to be passed along (JSON '
'is not checked, and is the responsibility of the ' 'format). Validity is against catalog requirements '
'calling client.'), 'is not checked, and is the responsibility of the '
'calling client.'),
}) })
add_url_response = document_ns.model('AddURLResponse', { add_url_response = document_ns.model('AddURLResponse', {
@@ -257,8 +261,10 @@ class AddURL(Resource):
@document_ns.response(500, 'Internal Server Error') @document_ns.response(500, 'Internal Server Error')
def post(self): def post(self):
""" """
Add a new document from URL. The URL in this case is stored and can be used to refresh the document. Add a new document to EveAI from a permanent URL.
As a consequence, this must be a permanent and accessible URL.
This endpoint is used for URLs that will remain accessible. The URL is stored and can
be used to refresh the document's content later.
""" """
tenant_id = get_jwt_identity() tenant_id = get_jwt_identity()
current_app.logger.info(f'Adding document from URL for tenant {tenant_id}') current_app.logger.info(f'Adding document from URL for tenant {tenant_id}')
@@ -383,7 +389,8 @@ class DocumentVersionResource(Resource):
"""Edit a document version""" """Edit a document version"""
data = request.json data = request.json
tenant_id = get_jwt_identity() tenant_id = get_jwt_identity()
updated_version, error = edit_document_version(tenant_id, version_id, data['user_context'], data.get('catalog_properties')) updated_version, error = edit_document_version(tenant_id, version_id, data['user_context'],
data.get('catalog_properties'))
if updated_version: if updated_version:
return {'message': f'Document Version {updated_version.id} updated successfully'}, 200 return {'message': f'Document Version {updated_version.id} updated successfully'}, 200
else: else:
@@ -518,58 +525,87 @@ class DocumentLookup(Resource):
return {'message': f'Missing required field: {str(e)}'}, 400 return {'message': f'Missing required field: {str(e)}'}, 400
refresh_content_model = document_ns.model('RefreshDocumentContent', { refresh_url_model = document_ns.model('RefreshDocumentThroughURL', {
'file_content': fields.Raw(required=True, description='The new file content'), 'temp_url': fields.String(required=True, description='Temporary URL of the updated document content'),
'language': fields.String(required=False, description='Language of the document'), 'language': fields.String(required=False, description='Language of the document'),
'user_context': fields.String(required=False, description='User context for the document'), 'user_context': fields.String(required=False, description='User context for the document'),
'user_metadata': fields.Raw(required=False, description='Custom metadata fields'), 'user_metadata': fields.Raw(required=False, description='Custom metadata fields'),
'catalog_properties': fields.Raw(required=False, description='Catalog-specific properties'), 'catalog_properties': fields.Raw(required=False, description='Catalog-specific properties'),
'trigger_service': fields.String(required=False, description='Service that triggered the update')
}) })
@document_ns.route('/<int:document_id>/refresh_content') @document_ns.route('/<int:document_id>/refresh_through_url')
class RefreshDocumentContent(Resource): class RefreshDocumentThroughURL(Resource):
@jwt_required() @jwt_required()
@requires_service('DOCAPI') @requires_service('DOCAPI')
@document_ns.expect(refresh_content_model) @document_ns.expect(refresh_url_model)
@document_ns.response(200, 'Document refreshed successfully') @document_ns.response(200, 'Document refreshed successfully')
def post(self, document_id): def post(self, document_id):
"""Refresh a document with new content""" """Refresh a document using content from a URL"""
tenant_id = get_jwt_identity() tenant_id = get_jwt_identity()
current_app.logger.info(f'Refreshing document {document_id} through URL for tenant {tenant_id}')
try: try:
data = request.json # Get filename from the existing version
file_content = data['file_content'] old_doc_vers = (DocumentVersion.query.filter_by(doc_id=document_id).
order_by(desc(DocumentVersion.id)).first())
filename = f"{old_doc_vers.id}.{old_doc_vers.file_type}"
# Build user_metadata by merging: args = request.json
# 1. Existing metadata (if any) user_metadata = json.loads(args.get('user_metadata', '{}'))
# 2. New metadata from request
# 3. Zapier-specific fields
user_metadata = data.get('user_metadata', {})
user_metadata.update({
'source': 'zapier',
'trigger_service': data.get('trigger_service')
})
data['user_metadata'] = user_metadata
# Keep catalog_properties separate try:
if 'catalog_properties' in data: actual_file_content, actual_file_content_type = download_file_content(args['temp_url'], user_metadata)
# We could add validation here against catalog configuration file_content = io.BytesIO(actual_file_content)
data['catalog_properties'] = data['catalog_properties'] file = FileStorage(
stream=file_content,
filename=filename,
content_type=actual_file_content_type
)
new_version, task_id = refresh_document_with_content( new_version, task_id = refresh_document_with_content(
document_id, document_id,
tenant_id, tenant_id,
file_content, actual_file_content,
data args
) )
return { return {
'message': f'Document refreshed successfully. New version: {new_version.id}. Task ID: {task_id}', 'message': f'Document refreshed successfully. New version: {new_version.id}. Task ID: {task_id}',
'document_id': document_id, 'document_id': document_id,
'document_version_id': new_version.id, 'document_version_id': new_version.id,
'task_id': task_id 'task_id': task_id
}, 200 }, 200
except requests.RequestException as e:
current_app.logger.error(f"Error downloading file: {str(e)}")
return {'message': f'Error downloading file: {str(e)}'}, 422
except EveAIException as e: except EveAIException as e:
return e.to_dict(), e.status_code return e.to_dict(), e.status_code
def download_file_content(url: str, user_metadata: dict) -> tuple[Any, Any]:
if user_metadata and 'service' in user_metadata and 'Zapier' in user_metadata['service']:
# Zapier uses a system of Stashed URLs
# Step 1: Download from stashed URL
stashed_url = url
current_app.logger.info(f"Downloading stashed file from URL: {stashed_url}")
response = requests.get(stashed_url, stream=True)
response.raise_for_status()
hydration_url = response.text.strip()
current_app.logger.info(f"Downloading actual file from URL: {hydration_url}")
# Step 2: Download from hydration URL
actual_file_response = requests.get(hydration_url, stream=True)
actual_file_response.raise_for_status()
actual_file_content = actual_file_response.content
else:
actual_url = url
actual_file_response = requests.get(actual_url, stream=True)
actual_file_response.raise_for_status()
actual_file_content = actual_file_response.content
actual_file_content_type = actual_file_response.headers.get('content-type', 'application/octet-stream')
return actual_file_content, actual_file_content_type

View File

@@ -35,6 +35,7 @@ def ping():
@current_celery.task(name='create_embeddings', queue='embeddings') @current_celery.task(name='create_embeddings', queue='embeddings')
def create_embeddings(tenant_id, document_version_id): def create_embeddings(tenant_id, document_version_id):
document_version = None
try: try:
# Retrieve Tenant for which we are processing # Retrieve Tenant for which we are processing
tenant = Tenant.query.get(tenant_id) tenant = Tenant.query.get(tenant_id)
@@ -66,6 +67,8 @@ def create_embeddings(tenant_id, document_version_id):
f'for badly configured document version {document_version_id} ' f'for badly configured document version {document_version_id} '
f'for tenant {tenant_id}, ' f'for tenant {tenant_id}, '
f'error: {e}') f'error: {e}')
if document_version:
document_version.processing_error = str(e)
raise raise
# BusinessEvent creates a context, which is why we need to use it with a with block # BusinessEvent creates a context, which is why we need to use it with a with block

View File

@@ -138,7 +138,6 @@ module.exports = {
// Get the file URL from Zapier // Get the file URL from Zapier
const tempFileUrl = await z.stashFile(bundle.inputData.file); const tempFileUrl = await z.stashFile(bundle.inputData.file);
// Log the temporary URL for debugging
z.console.log('Temporary URL created:', tempFileUrl); z.console.log('Temporary URL created:', tempFileUrl);
// Create request data as an object // Create request data as an object
@@ -190,73 +189,5 @@ module.exports = {
throw error; throw error;
} }
} }
// perform: async (z, bundle) => {
// try {
// z.console.log("Starting New Log Trace for add_document")
// z.console.log("=======================================")
//
// // Prepare base metadata
// const baseMetadata = {
// service: bundle.inputData.metadata_service || 'Zapier',
// source: bundle.inputData.metadata_source,
// unique_id: bundle.inputData.metadata_unique_id,
// unique_url: bundle.inputData.metadata_unique_url,
// };
//
// // If there's additional metadata, merge it with the base metadata
// if (bundle.inputData.additional_metadata) {
// Object.assign(baseMetadata, bundle.inputData.additional_metadata);
// }
//
// const requestData = {
//
// catalog_id: bundle.inputData.catalog_id,
// language: bundle.inputData.language,
//
// // Add optional fields if they exist
// name: bundle.inputData.name || undefined,
// user_context: bundle.inputData.user_context || undefined,
// valid_from: bundle.inputData.valid_from || undefined,
// user_metadata: JSON.stringify(baseMetadata),
// catalog_properties: JSON.stringify(bundle.inputData.catalog_properties) || undefined,
// file: z.stashFile(bundle.inputData.file),
// }
//
// // Make request to your API
// const response = await z.request({
// url: 'https://evie.askeveai.com/api/api/v1/documents/add_document',
// method: 'POST',
// body: requestData,
// headers: {
// 'Authorization': `Bearer ${bundle.authData.access_token}`,
// 'Content-Type': 'multipart/form-data',
// },
// });
//
// // Log the response for debugging
// z.console.log('API Response:', {
// status: response.status,
// body: response.data
// });
// // Return the parsed response
// return response.json;
// } catch (error) {
// // Enhanced error logging
// z.console.error('Error details:', {
// message: error.message,
// response: error.response ? {
// status: error.response.status,
// headers: error.response.headers,
// data: error.response.data
// } : 'No response',
// request: error.request ? {
// method: error.request.method,
// url: error.request.url,
// headers: error.request.headers
// } : 'No request'
// });
// throw error;
// }
// }
} }
}; };

View File

@@ -0,0 +1,160 @@
const EveAIApiClient = require('../api_client');
module.exports = {
display: {
description: "Refresh an existing document in Evie's Library with new content",
hidden: false,
label: 'Refresh Document in Evie',
},
key: 'refresh_document',
noun: 'Document',
operation: {
inputFields: [
{
key: 'file',
label: 'The Updated File',
type: 'file',
helpText: "The new content to replace the existing document",
required: true,
},
{
key: 'language',
label: 'Document Language',
type: 'string',
default: 'en',
helpText: 'Two-letter-code of the language the document is written in.',
required: true,
},
{
key: 'user_context',
label: 'User Context',
type: 'text',
helpText:
'Contextual information you want to add to the Document. If you have structured information to be shared, you can better add this information to the User Metadata, which allows for json to be uploaded.',
required: false,
list: false,
altersDynamicFields: false,
},
{
key: 'metadata_service',
label: 'Service',
type: 'string',
default: 'Zapier',
helpText: "By default we use 'Zapier' as service name. However, if you need to change that to e.g. give an indication of the Zapier flow, you can change this value.",
required: true,
},
{
key: 'metadata_source',
label: 'Source App',
type: 'string',
helpText: "The source app of the document's origin. e.g. 'Dropbox' if the document is provided through Dropbox, or 'Google Docs' if that happens to be the origin of the document.",
required: false,
},
{
key: 'metadata_unique_id',
label: 'Unique ID',
type: 'string',
helpText: 'An unique identifier, provided by the source system, if that is available.',
required: false,
},
{
key: 'metadata_unique_url',
label: 'Unique URL',
type: 'string',
helpText: "A unique URL that is provided by the source system, if that's available",
required: false,
},
{
key: 'additional_metadata',
label: 'Additional Metadata',
helpText: "Extra metadata you'd like to add to the document",
dict: true,
required: false,
altersDynamicFields: false,
},
{
key: 'catalog_properties',
label: 'Catalog Properties',
helpText:
'Depending on the Catalog ID provided, you can add the required key-value pairs here.',
dict: true,
required: false,
altersDynamicFields: false,
},
],
perform: async (z, bundle) => {
try {
z.console.log("Starting New Log Trace for refresh_document");
z.console.log("===========================================");
const client = new EveAIApiClient(z, bundle);
// Prepare base metadata
const baseMetadata = {
service: bundle.inputData.metadata_service || 'Zapier',
source: bundle.inputData.metadata_source || '',
unique_id: bundle.inputData.metadata_unique_id || '',
unique_url: bundle.inputData.metadata_unique_url || '',
};
// If there's additional metadata, merge it
if (bundle.inputData.additional_metadata) {
Object.assign(baseMetadata, bundle.inputData.additional_metadata);
}
// First, lookup the document by unique_id
const lookupResponse = await client.make_request('POST', '/documents/lookup', {
lookup_criteria: { unique_id: bundle.inputData.metadata_unique_id },
metadata_type: 'user_metadata'
});
const documentId = lookupResponse.document_id;
z.console.log("Found Document ID: ", documentId)
// Get the temporary URL from Zapier's file storage
const tempFileUrl = await z.stashFile(bundle.inputData.file);
z.console.log('Temporary URL created:', tempFileUrl);
// Prepare the refresh request
const requestData = {
temp_url: tempFileUrl,
language: bundle.inputData.language,
user_metadata: JSON.stringify(baseMetadata),
};
// Add user_context property if it exists
if (bundle.inputData.user_context) {
requestData.user_context = bundle.inputData.user_context;
}
// Add catalog properties if they exist
if (bundle.inputData.catalog_properties) {
requestData.catalog_properties = JSON.stringify(bundle.inputData.catalog_properties);
}
// Make the refresh request
return await client.make_request(
'POST',
`/documents/${documentId}/refresh_through_url`,
requestData
);
} catch (error) {
z.console.error('Error details:', {
message: error.message,
response: error.response ? {
status: error.response.status,
headers: error.response.headers,
data: error.response.data
} : 'No response',
request: error.request ? {
method: error.request.method,
url: error.request.url,
headers: error.request.headers
} : 'No request'
});
throw error;
}
},
},
};

View File

@@ -1,5 +1,6 @@
const authentication = require('./authentication'); const authentication = require('./authentication');
const addDocument = require('./creates/add_document'); const addDocument = require('./creates/add_document');
const refreshDocument = require('./creates/refresh_document'); // Add this line
module.exports = { module.exports = {
// This is just shorthand to reference the installed dependencies you have. // This is just shorthand to reference the installed dependencies you have.
@@ -18,7 +19,8 @@ module.exports = {
// If you want your creates to show up, you better include it here! // If you want your creates to show up, you better include it here!
creates: { creates: {
[addDocument.key]: addDocument [addDocument.key]: addDocument,
[refreshDocument.key]: refreshDocument,
}, },
resources: {}, resources: {},

View File

@@ -1,13 +1,13 @@
{ {
"name": "eveai_integration", "name": "eveai_integration",
"version": "1.0.3", "version": "1.0.5",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "jest --testTimeout 10000" "test": "jest --testTimeout 10000"
}, },
"dependencies": { "dependencies": {
"zapier-platform-core": "15.19.0" "zapier-platform-core": "16.0.0"
}, },
"devDependencies": { "devDependencies": {
"jest": "^29.6.0" "jest": "^29.6.0"