#!/usr/bin/env python3 """Build standalone OpenWebUI Tools archives; generated files stay in data/.""" from __future__ import annotations import argparse import ast import hashlib import importlib.util import json from pathlib import Path import sys import zipfile ROOT = Path(__file__).resolve().parents[1] ENGINES = ('rag', 'graphrag') def json_bytes(value) -> bytes: return (json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + '\n').encode() def build_package(engine: str, output: Path) -> dict: if engine not in ENGINES: raise ValueError('Unknown tool package') source = ROOT / 'packages/openwebui' / engine / 'tool.py' content = source.read_bytes() header = ast.get_docstring(ast.parse(content.decode())) metadata = dict(line.strip().split(': ', 1) for line in header.splitlines() if ': ' in line) name = 'zpwiki_package_' + engine spec = importlib.util.spec_from_file_location(name, source) module = importlib.util.module_from_spec(spec) sys.modules[name] = module try: spec.loader.exec_module(module) config = module.Tools.Valves().model_dump() schema = module.Tools.Valves.model_json_schema() finally: sys.modules.pop(name, None) instructions = ( f'ZP Wiki {engine.upper()} – OpenWebUI Tool\n\n' 'ZIP najprv rozbal. Vo Workspace > Tools > Import vyber openwebui-tool.json.\n' 'Alebo cez Create vytvor nástroj a vlož celý obsah tool.py do editora.\n' 'Pri importe cez URL použi raw URL súboru tool.py, nie ZIP ani stránku repozitára.\n' 'Vo Valves nastav BACKEND_URL a BACKEND_API_KEY = SEARCH_API_KEY backendu.\n' 'Adresa musí byť dostupná zo servera OpenWebUI; neobsahuje koncový endpoint.\n' 'Backend aj oba nástroje používajú rovnaký dátový kontrakt.\n' 'Výstup sú vyhľadané podklady, nie schválený finálny text.\n' 'Pre kontrolované doručenie používaj existujúci Pipe a /answer.\n' 'Žiadny ďalší nástroj ani lokálny Python balík zp-agent nie je potrebný.\n' 'Podrobný postup, testy a obmedzenia sú v hlavnom README projektu.\n') import_form = [{'id': module.TOOL_ID, 'name': metadata['title'], 'content': content.decode(), 'meta': {'description': metadata['description'], 'manifest': metadata}, 'access_grants': []}] files = {'tool.py': content, 'openwebui-tool.json': json_bytes(import_form), 'config.example.json': json_bytes(config), 'valves.schema.json': json_bytes(schema), 'INSTALL.txt': instructions.encode()} manifest = {'schema_version': 1, 'kind': 'openwebui_tool', 'package_id': module.TOOL_ID, 'version': metadata['version'], 'minimum_openwebui_version': metadata['required_open_webui_version'], 'backend_endpoint': module.ENDPOINT, 'entrypoint': 'tool.py', 'output_kind': 'retrieval_context', 'files': {name: hashlib.sha256(data).hexdigest() for name, data in files.items()}} files['manifest.json'] = json_bytes(manifest) output.mkdir(parents=True, exist_ok=True) destination = output / (module.TOOL_ID + '.zip') import_file = output / (module.TOOL_ID + '.json') import_file.write_bytes(files['openwebui-tool.json']) # Fixed timestamps and sorted entries make a rebuild byte-for-byte identical. with zipfile.ZipFile(destination, 'w', compression=zipfile.ZIP_DEFLATED) as archive: for name, data in sorted(files.items()): info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0)) info.compress_type = zipfile.ZIP_DEFLATED info.create_system = 3 info.external_attr = 0o100644 << 16 archive.writestr(info, data) return {'package_id': module.TOOL_ID, 'archive': str(destination), 'import_file': str(import_file), 'sha256': hashlib.sha256(destination.read_bytes()).hexdigest(), 'manifest': manifest} def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--output', type=Path, default=ROOT / 'data/tool_packages') args = parser.parse_args() built = [build_package(engine, args.output) for engine in ENGINES] (args.output / 'packages.json').write_bytes(json_bytes(built)) print(json.dumps([{key: row[key] for key in ['package_id', 'archive', 'import_file', 'sha256']} for row in built], ensure_ascii=False)) if __name__ == '__main__': main()