mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-28 05:27:35 +00:00
commit
6adde203cd
234 changed files with 18048 additions and 6117 deletions
118
CHANGELOG.md
118
CHANGELOG.md
|
|
@ -5,6 +5,124 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.7.0] - 2026-01-09
|
||||
|
||||
### Added
|
||||
|
||||
- 🤖 **Native Function Calling with Built-in Tools.** Users can now ask models to perform multi-step tasks that combine web research, knowledge base queries, note-taking, and image generation in a single conversation—for example, "research the latest on X, save key findings to a note, and generate an infographic." Requires models with native function calling support and function calling mode set to "Native" in Chat Controls. [#19397](https://github.com/open-webui/open-webui/issues/19397), [Commit](https://github.com/open-webui/open-webui/commit/5c1d52231a3997a17381c48639bd7e339262cf7c)
|
||||
- 🧠 Users can now ask the model to find relevant context from their notes, past chats, and channel messages—for example, "what did I discuss about project X last week?" or "find the conversation where I brainstormed ideas for Y." [Commit](https://github.com/open-webui/open-webui/commit/646835d76744ad9b2e67ede0407a61d62e969aab)
|
||||
- 📚 Users can now ask the model to search their knowledge bases and retrieve documents without manually attaching files—for example, "find the section about authentication in our API docs" or "what do our internal guidelines say about X?" [Commit](https://github.com/open-webui/open-webui/commit/c8622adcb01f3091b17ca50f8c8e2f20c7b9cd2a)
|
||||
- 💭 Users with models that support interleaved thinking now get more refined results from multi-step workflows, as the model can analyze each tool's output before deciding what to do next.
|
||||
- 🔍 When models invoke web search, search results appear as clickable citations in real-time for full source verification. [Commit](https://github.com/open-webui/open-webui/commit/2789f6a24d8405c30cd48ae460071f6a4f2c35f9)
|
||||
- 🎚️ Users can selectively disable specific built-in tools (timestamps, memory, chat history, notes, web search, knowledge bases) per model via the model editor's capabilities settings. [Commit](https://github.com/open-webui/open-webui/commit/60e916d6c0c5f7db9e6d670e12be2d1d4abc2dd6)
|
||||
- 👁️ Pending tool calls are now displayed during response generation, so users know which tools are being invoked. [Commit](https://github.com/open-webui/open-webui/commit/1d08376860e775049abd1dd5f568ac0c6466c944)
|
||||
- 📁 Administrators can now limit the number of files that can be uploaded to folders using the "FOLDER_MAX_FILE_COUNT" setting, preventing resource exhaustion from bulk uploads. [#19810](https://github.com/open-webui/open-webui/issues/19810), [Commit](https://github.com/open-webui/open-webui/commit/a1036e544d573e3d35e05c1c2472ba762c32431b), [Commit](https://github.com/open-webui/open-webui/commit/d3ee3fd23e762c9d83fe1da5636d03259e186e57)
|
||||
- ⚡ Users experience transformative speed improvements across the entire application through completely reengineered database connection handling, delivering noticeably faster page loads, butter-smooth interactions, and rock-solid stability during intensive operations like user management and bulk data processing. [Commit](https://github.com/open-webui/open-webui/commit/2041ab483e21b3a757baa25c47dc2fa29018674f), [Commit](https://github.com/open-webui/open-webui/commit/145c7516f227ce56fd52373cee86217fadf16181), [Commit](https://github.com/open-webui/open-webui/commit/475dd91ed798f2efcdf27799d5c5cae3f0e6e847), [Commit](https://github.com/open-webui/open-webui/commit/5d1459df166cce8445eb1556cc26abbd65a3f9f4), [Commit](https://github.com/open-webui/open-webui/commit/2453b75ff0fb2dc75b929d96f417d84e332922d9), [Commit](https://github.com/open-webui/open-webui/commit/5649a668fad15393a52c27a2f188841af8b66989)
|
||||
- 🚀 Users experience significantly faster initial page load times through dynamic loading of document processing libraries, reducing the initial bundle size. [#20200](https://github.com/open-webui/open-webui/pull/20200), [#20202](https://github.com/open-webui/open-webui/pull/20202), [#20203](https://github.com/open-webui/open-webui/pull/20203), [#20204](https://github.com/open-webui/open-webui/pull/20204)
|
||||
- 💨 Administrators experience dramatically faster user list loading through optimized database queries that eliminate N+1 query patterns, reducing query count from 1+N to just 2 total queries regardless of user count. [#20427](https://github.com/open-webui/open-webui/pull/20427)
|
||||
- 📋 Notes now load faster through optimized database queries that batch user lookups instead of fetching each note's author individually. [Commit](https://github.com/open-webui/open-webui/commit/084f0ef6a5491e186bf6b71c6386973ba18ef2fa)
|
||||
- 💬 Channel messages, pinned messages, and thread replies now load faster through batched user lookups instead of individual queries per message. [#20458](https://github.com/open-webui/open-webui/pull/20458), [#20459](https://github.com/open-webui/open-webui/pull/20459), [#20460](https://github.com/open-webui/open-webui/pull/20460)
|
||||
- 🔗 Users can now click citation content links to jump directly to the relevant portion of source documents with automatic text highlighting, making it easier to verify AI responses against their original sources. [#20116](https://github.com/open-webui/open-webui/pull/20116), [Commit](https://github.com/open-webui/open-webui/commit/40c45ffe1f9b45538d32c8ecba8cac62c6eca503)
|
||||
- 📌 Users can now pin or hide models directly from the Workspace Models page and Admin Settings Models page, making it easier to manage which models appear in the sidebar without switching to the chat interface. [#20176](https://github.com/open-webui/open-webui/pull/20176)
|
||||
- 🔎 Administrators can now quickly find settings using the new search bar in the Admin Settings sidebar, which supports fuzzy filtering by category names and related keywords like "whisper" for Audio or "rag" for Documents. [#20434](https://github.com/open-webui/open-webui/pull/20434)
|
||||
- 🎛️ Users can now view read-only models in the workspace models list, with clear "Read Only" badges indicating when editing is restricted. [#20243](https://github.com/open-webui/open-webui/issues/20243), [#20369](https://github.com/open-webui/open-webui/pull/20369)
|
||||
- 📝 Users can now view read-only prompts in the workspace prompts list, with clear "Read Only" badges indicating when editing is restricted. [#20368](https://github.com/open-webui/open-webui/pull/20368)
|
||||
- 🔧 Users can now view read-only tools in the workspace tools list, with clear "Read Only" badges indicating when editing is restricted. [#20243](https://github.com/open-webui/open-webui/issues/20243), [#20370](https://github.com/open-webui/open-webui/pull/20370)
|
||||
- 📂 Searching for files is now significantly faster, especially for users with large file collections. [Commit](https://github.com/open-webui/open-webui/commit/a9a979fb3db1743553ca0705f571c0b9c252841f)
|
||||
- 🏆 The Evaluations leaderboard now calculates Elo ratings on the backend instead of in the browser, improving performance and enabling topic-based model ranking through semantic search. [#15392](https://github.com/open-webui/open-webui/pull/15392), [#20476](https://github.com/open-webui/open-webui/issues/20476), [Commit](https://github.com/open-webui/open-webui/commit/10838b3654bf6fdef02d57311f7f1c01df4cd033)
|
||||
- 📊 The Evaluations leaderboard now includes a per-model activity chart displaying daily wins and losses as a diverging bar chart, with 30-day, 1-year, and all-time views using weekly aggregation for longer timeframes.
|
||||
- 🎞️ Users can now upload animated GIF and WebP formats as model profile images, with animation preserved by skipping resize processing for these file types. [Commit](https://github.com/open-webui/open-webui/commit/00af37bb4ed1ea0957c7c84a6a8def3a7998b8ca)
|
||||
- 📸 Users uploading profile images for users, models, and arena models now benefit from WebP compression at 80% quality instead of JPEG, resulting in significantly smaller file sizes and faster uploads while maintaining visual quality. [Commit](https://github.com/open-webui/open-webui/commit/b1d30673b69571e081abb881d34944cf33cdc67e)
|
||||
- ⭐ Action Function developers can now update message favorite status using the new "chat:message:favorite" event, enabling the development of pin/unpin message actions without race conditions from frontend auto-save. [#20375](https://github.com/open-webui/open-webui/pull/20375)
|
||||
- 🌐 Users with OpenAI-compatible models that have web search capabilities now see URL citations displayed as sources in the interface. [#20172](https://github.com/open-webui/open-webui/pull/20172), [Commit](https://github.com/open-webui/open-webui/commit/fe84afd09a2bc8a89311f30186ec2608a4edda3a)
|
||||
- 📰 Users can now dismiss the "What's New" changelog modal permanently using the X button, matching the behavior of the "Okay, Let's Go!" button. [#20258](https://github.com/open-webui/open-webui/pull/20258)
|
||||
- 📧 Administrators can now configure the admin contact email displayed in the Account Pending overlay directly from the Admin Panel instead of only through environment variables. [#12500](https://github.com/open-webui/open-webui/issues/12500), [#20260](https://github.com/open-webui/open-webui/pull/20260)
|
||||
- 📄 Administrators can now enable markdown header text splitting as a preprocessing step that works with either character or token splitting, through the new "ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER" setting. [Commit](https://github.com/open-webui/open-webui/commit/d3a682759f375c6cb0bc8c170f62863a070f712e), [Commit](https://github.com/open-webui/open-webui/commit/18a33a079bf07487edffc421a721c86194cc90c9), [Commit](https://github.com/open-webui/open-webui/commit/08bf4670ec862018f9dc57296cb19fd5eab14ef6)
|
||||
- 🧩 Administrators can now set a minimum chunk size target using the "CHUNK_MIN_SIZE_TARGET" setting to merge small markdown header chunks with neighbors, which improves retrieval quality by eliminating tiny meaningless fragments, significantly speeds up document processing and embedding, reduces storage costs, and lowers embedding API costs or local compute requirements. [#19595](https://github.com/open-webui/open-webui/issues/19595), [#20314](https://github.com/open-webui/open-webui/pull/20314), [Commit](https://github.com/open-webui/open-webui/commit/c32435958073cf002d87e78544baa88bc4e15d7f)
|
||||
- 💨 Administrators can now enable KV prefix caching optimization by setting "RAG_SYSTEM_CONTEXT" to true, which injects RAG context into the system message instead of user messages, enabling models to reuse cached tokens for follow-up questions instead of reprocessing the entire context on each turn, significantly improving response times and reducing costs for cloud-based models. [#20301](https://github.com/open-webui/open-webui/discussions/20301), [#20317](https://github.com/open-webui/open-webui/pull/20317)
|
||||
- 🖼️ Administrators and Action developers can now control image generation denoising steps per-request using a steps parameter, allowing Actions and API calls to override the global IMAGE_STEPS configuration for both ComfyUI and Automatic1111 engines. [#20337](https://github.com/open-webui/open-webui/pull/20337)
|
||||
- 🗄️ Administrators running multi-pod deployments can now designate a master pod to handle database migrations using the "ENABLE_DB_MIGRATIONS" environment variable. [Commit](https://github.com/open-webui/open-webui/commit/9824f0e33359a917ac07b60bf1f972074d5c8203)
|
||||
- 🎙️ Administrators can now configure Whisper's compute type using the "WHISPER_COMPUTE_TYPE" environment variable to fix compatibility issues with CUDA/GPU deployments. [Commit](https://github.com/open-webui/open-webui/commit/26af1f92e21ddfd08348570bf54a6f345ac69648)
|
||||
- 🔍 Administrators can now control sigmoid normalization for CrossEncoder reranking models using the "SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION" environment variable, enabled by default for proper relevance threshold behavior with MS MARCO models. [#20228](https://github.com/open-webui/open-webui/pull/20228)
|
||||
- 🔒 Administrators can now disable SSL certificate verification for external tools using the "REQUESTS_VERIFY" environment variable, enabling integration with self-signed certificates for Tika, Ollama embeddings, and external rerankers. [#19968](https://github.com/open-webui/open-webui/issues/19968), [Commit](https://github.com/open-webui/open-webui/commit/dfc5dad63167eabb7fb027e63c324675b23f2e9d)
|
||||
- 📈 Administrators can now control audit log output destinations using "ENABLE_AUDIT_STDOUT" and "ENABLE_AUDIT_LOGS_FILE" environment variables, allowing audit logs to be sent to container logs for centralized logging systems. [#20114](https://github.com/open-webui/open-webui/pull/20114), [Commit](https://github.com/open-webui/open-webui/commit/fdae5644e36972384b3e2513e3074f95f9f7381f)
|
||||
- 🛡️ Administrators can now restrict non-admin user access to Interface Settings through per-user or per-group permissions. [#20424](https://github.com/open-webui/open-webui/pull/20424)
|
||||
- 🧠 Administrators can now globally enable or disable the Memories feature and control access through per-user or per-group permissions, with the Personalization tab automatically hidden when the feature is disabled. [#20462](https://github.com/open-webui/open-webui/pull/20462)
|
||||
- 🟢 Administrators can now globally enable or disable user status visibility through the "ENABLE_USER_STATUS" setting in Admin Settings. [#20488](https://github.com/open-webui/open-webui/pull/20488)
|
||||
- 🪝 Channel managers can now create webhooks to allow external services to post messages to channels without authentication. [Commit](https://github.com/open-webui/open-webui/commit/cd296fcf0d79cecd1a6a3ee4e492c6b5246ca7ae)
|
||||
- 📄 In the model editor users can now disable the "File Context" capability to skip automatic file content extraction and injection, forwarding raw messages with file attachment metadata instead for use with custom tools or future built-in file access tools. [Commit](https://github.com/open-webui/open-webui/commit/daccf0713e3ecd6d24f003a87b5f8b3c61852958), [Docs:Commit](https://github.com/open-webui/docs/commit/18ec6eaefc071a278ec57d4d1b8d66d686af0870)
|
||||
- 🔊 In the model editor users can now configure a specific TTS voice for each model, overriding user preferences and global defaults to give different AI personas distinct voices. [#3097](https://github.com/open-webui/open-webui/issues/3097), [Commit](https://github.com/open-webui/open-webui/commit/bb6188abf04302f79d80b0d6cc42c232624b5757)
|
||||
- 👥 Administrators now have three granular group sharing permission options instead of a simple on/off toggle, allowing them to choose between "No one", "Members", or "Anyone" for who can share content to each group. [Commit](https://github.com/open-webui/open-webui/commit/ca514cd3eda2524b8da472ef17c0ccb216bac2e8)
|
||||
- 📦 Administrators can now export knowledge bases as zip files containing text files for backup and archival purposes. [#20120](https://github.com/open-webui/open-webui/issues/20120), [Commit](https://github.com/open-webui/open-webui/commit/c1147578c073a8c7fa7e7f836149e1cdfec8f18d)
|
||||
- 🚀 Administrators can now create an admin account automatically at startup via "WEBUI_ADMIN_EMAIL", "WEBUI_ADMIN_PASSWORD", and "WEBUI_ADMIN_NAME" environment variables, enabling headless and automated deployments without exposing the signup page. [#17654](https://github.com/open-webui/open-webui/issues/17654), [Commit](https://github.com/open-webui/open-webui/commit/1138929f4d083931305f1f925899971b190562ae)
|
||||
- 🦆 Administrators can now select a specific search backend for DDGS instead of random selection, with options including Bing, Brave, DuckDuckGo, Google, Wikipedia, Yahoo, and others. [#20330](https://github.com/open-webui/open-webui/issues/20330), [#20366](https://github.com/open-webui/open-webui/pull/20366)
|
||||
- 🧭 Administrators can now configure custom Jina Search API endpoints using the "JINA_API_BASE_URL" environment variable, enabling region-specific deployments such as EU data processing. [#19718](https://github.com/open-webui/open-webui/pull/19718), [Commit](https://github.com/open-webui/open-webui/commit/f7f8a263b92289df8d4f8dbc3bae09bd009a5699)
|
||||
- 🔥 Administrators can now configure Firecrawl timeout values using the "FIRECRAWL_TIMEOUT" environment variable to control web scraping wait times. [#19973](https://github.com/open-webui/open-webui/pull/19973), [Commit](https://github.com/open-webui/open-webui/commit/89ad1c68d1aadf849960b5e202aa4651096b05f5)
|
||||
- 💾 Administrators can now use openGauss as the vector database backend for knowledge base document storage and retrieval. [#20179](https://github.com/open-webui/open-webui/pull/20179)
|
||||
- 🔄 Various improvements were implemented across the application to enhance performance, stability, and security.
|
||||
- 📊 Users can now sync their anonymous usage statistics to the Open WebUI Community platform to power community leaderboards, drive model evaluations, and contribute to open-source AI research that benefits everyone, all while keeping conversations completely private (only metadata like model names, message counts, and ratings are shared). By sharing your stats, you're helping the community identify which models perform best, contributing to transparent AI benchmarking, and supporting the collective effort to make AI better for all. You can also download your stats as JSON for personal analysis.
|
||||
- 🌐 Translations for German, Portuguese (Brazil), Spanish, Simplified Chinese, Traditional Chinese, and Polish were enhanced and expanded.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🔊 Text-to-speech now correctly splits on newlines in addition to punctuation, so markdown bullet points and lists are spoken as separate sentences instead of being merged together. [#5924](https://github.com/open-webui/open-webui/issues/5924), [Commit](https://github.com/open-webui/open-webui/commit/869108a3e1ce2b8110084113c1b392072e98fd5f)
|
||||
- 🔒 Users are now protected from stored XSS vulnerabilities in iFrame embeds for citations and response messages through configurable same-origin sandbox settings instead of hardcoded values. [#20209](https://github.com/open-webui/open-webui/pull/20209), [#20210](https://github.com/open-webui/open-webui/pull/20210)
|
||||
- 🔑 Image Generation, Web Search, and Audio (TTS/STT) API endpoints now enforce permission checks on the backend, closing a security gap where disabled features could previously be accessed via direct API calls. [#20471](https://github.com/open-webui/open-webui/pull/20471)
|
||||
- 🛠️ Tools and Tool Servers (MCP and OpenAPI) now enforce access control checks on the backend, ensuring users can only access tools they have permission to use even via direct API calls. [#20443](https://github.com/open-webui/open-webui/issues/20443), [Commit](https://github.com/open-webui/open-webui/commit/9b06fdc8fe1c933071610336be05f11e77e6c8eb)
|
||||
- 🔁 System prompts are no longer duplicated when using native function calling, fixing an issue where the prompt would be applied twice during tool-calling workflows. [Commit](https://github.com/open-webui/open-webui/commit/9223efaff0db6e56bfa157ef214d9590005156d2)
|
||||
- 🗂️ Knowledge base uploads to folders no longer fail when "FOLDER_MAX_FILE_COUNT" is unset, fixing an issue where the default null value caused all uploads to error. [Commit](https://github.com/open-webui/open-webui/commit/ef9cd0e0ad6e45b8a3efec6f3858b3d69d42f619)
|
||||
- 📝 The "Create Note" button in the chat input now correctly hides for users without Notes permissions instead of showing and returning a 401 error when clicked. [#20486](https://github.com/open-webui/open-webui/issues/20486), [Commit](https://github.com/open-webui/open-webui/commit/9e9616b670c1c4389193b18500a7d80d86d7e280)
|
||||
- 📊 The Evaluations page no longer crashes when administrators have large amounts of feedback data, as the leaderboard now fetches only the minimal required fields instead of loading entire conversation snapshots. [#20476](https://github.com/open-webui/open-webui/issues/20476), [#20489](https://github.com/open-webui/open-webui/pull/20489), [Commit](https://github.com/open-webui/open-webui/commit/b2a1f71d920e55b143f1c02e61104938d2588762)
|
||||
- 💬 Users can now export chats, use the Ask/Explain popup, and view chat lists correctly again after these features were broken by recent refactoring changes that caused 500 and 400 server errors. [#20146](https://github.com/open-webui/open-webui/issues/20146), [#20205](https://github.com/open-webui/open-webui/issues/20205), [#20206](https://github.com/open-webui/open-webui/issues/20206), [#20212](https://github.com/open-webui/open-webui/pull/20212)
|
||||
- 💭 Users no longer experience data corruption when switching between chats during background operations like image generation, where messages from one chat would appear in another chat's history. [#20266](https://github.com/open-webui/open-webui/pull/20266)
|
||||
- 🛡️ Users no longer encounter critical chat stability errors, including duplicate key errors from circular message dependencies, null message access during chat loading, and errors in the chat overview visualization. [#20268](https://github.com/open-webui/open-webui/pull/20268)
|
||||
- 📡 Users with Channels no longer experience infinite recursion and connection pool exhaustion when fetching threaded replies, preventing RecursionError crashes during chat history loading. [#20299](https://github.com/open-webui/open-webui/pull/20299), [Commit](https://github.com/open-webui/open-webui/commit/c144122f608759c2b79472e1f6948a7c1600a3d1)
|
||||
- 📎 Users no longer encounter TypeError crashes when viewing messages with file attachments that have undefined URL properties. [#20343](https://github.com/open-webui/open-webui/pull/20343)
|
||||
- 🔐 Users with MCP integrations now experience reliable OAuth 2.1 token refresh after access token expiration through proper Protected Resource discovery, preventing integration failures that caused sessions to be deleted. [#19794](https://github.com/open-webui/open-webui/issues/19794), [#20138](https://github.com/open-webui/open-webui/pull/20138), [#20291](https://github.com/open-webui/open-webui/issues/20291), [Commit](https://github.com/open-webui/open-webui/commit/bf2b2962399e341926bdbf9e0a82101f31a90b23), [Commit](https://github.com/open-webui/open-webui/commit/89565c58c6ae6b5b129559ef68b5a0c18c110765)
|
||||
- 📚 Users who belong to multiple groups can now see Knowledge Bases shared with those groups, fixing an issue where they would disappear when shared with more than one group. [#20124](https://github.com/open-webui/open-webui/issues/20124), [#20229](https://github.com/open-webui/open-webui/issues/20229), [Commit](https://github.com/open-webui/open-webui/commit/61e25dc2dce9c12dcb5b88a6b814060c4338e67b)
|
||||
- 📂 Users now see the correct Knowledge Base name when hovering over # file references in chat input instead of "undefined". [#20329](https://github.com/open-webui/open-webui/issues/20329), [#20333](https://github.com/open-webui/open-webui/pull/20333)
|
||||
- 📋 Users now see notes displayed in correct chronological order within their time range groupings, fixing an issue where insertion order was not preserved. [Commit](https://github.com/open-webui/open-webui/commit/3f577c0c3fbfd9f09c02940e4ae474f987149277)
|
||||
- 📑 Users collaborating on notes now experience proper content sync when initializing from both HTML and JSON formats, fixing sync failures in collaborative editing sessions. [Commit](https://github.com/open-webui/open-webui/commit/e27fb3e291a735c715a089a80e7a49d2c2209096)
|
||||
- 🔎 Users searching notes can now find hyphenated words and variations with spaces, so searching "todo" now finds "to-do" and "to do". [Commit](https://github.com/open-webui/open-webui/commit/a3270648d8b8535443d8ce2ea719f8e678e4e358)
|
||||
- 📥 Users no longer experience false duplicate file warnings when reuploading files after initial processing failed, as the file hash is now only stored after successful processing completion. [#19264](https://github.com/open-webui/open-webui/issues/19264), [#20282](https://github.com/open-webui/open-webui/pull/20282), [Commit](https://github.com/open-webui/open-webui/commit/d3ab9f4b96eee7f91c9b1355cee055fdabca9730)
|
||||
- 💾 Users experience significantly improved page load performance as model profile images now cache properly in browsers, avoiding unnecessary image refetches. [Commit](https://github.com/open-webui/open-webui/commit/bb821ab654e93908a3b4632c359753eeff053264)
|
||||
- 🎨 Users can now successfully edit uploaded images instead of having new images generated, fixing an issue introduced by the file storage refactor where images with type "file" and content_type starting with "image/" weren't being recognized as editable images. [#20237](https://github.com/open-webui/open-webui/issues/20237), [#20169](https://github.com/open-webui/open-webui/pull/20169), [#20239](https://github.com/open-webui/open-webui/pull/20239), [Commit](https://github.com/open-webui/open-webui/commit/1148d1c927d096e14917b6d762789fca3188f281)
|
||||
- 🌐 Users writing in Persian and Arabic now see properly displayed right-to-left text in the notes section through automatic text direction detection. [#19743](https://github.com/open-webui/open-webui/issues/19743), [#20102](https://github.com/open-webui/open-webui/pull/20102), [Commit](https://github.com/open-webui/open-webui/commit/b619a157bc54c5bc44d223d2ae3acb9ce4ac6a6c)
|
||||
- 🤖 Users can now successfully @ mention models in Channels instead of experiencing silent failures. [Commit](https://github.com/open-webui/open-webui/commit/59957715836acb635f4b1c4ddbfb4ba7b82b3281)
|
||||
- 📋 Users on Windows now see correctly preserved line breaks when using the {{CLIPBOARD}} variable through CRLF to LF normalization. [#19370](https://github.com/open-webui/open-webui/issues/19370), [#20283](https://github.com/open-webui/open-webui/pull/20283)
|
||||
- 📁 Users now see the Knowledge Selector dropdown correctly displayed above the Create Folder modal instead of being hidden behind it. [#20219](https://github.com/open-webui/open-webui/issues/20219), [#20213](https://github.com/open-webui/open-webui/pull/20213)
|
||||
- 🌅 Users now see profile images in non-PNG formats like SVG, JPEG, and GIF displayed correctly instead of appearing broken. [#20171](https://github.com/open-webui/open-webui/pull/20171)
|
||||
- 🆕 Non-admin users with disabled temporary chat permissions can now successfully create new chats and use pinned models from the sidebar. [#20336](https://github.com/open-webui/open-webui/issues/20336), [#20367](https://github.com/open-webui/open-webui/pull/20367), [Commit](https://github.com/open-webui/open-webui/commit/e754940c031f9689fb4f6edb3625aa06aeb53377)
|
||||
- 🎛️ Users can now successfully use workspace models in chat, fixing "Model not found" errors that occurred when using custom model presets. [#20340](https://github.com/open-webui/open-webui/issues/20340), [#20344](https://github.com/open-webui/open-webui/pull/20344), [Commit](https://github.com/open-webui/open-webui/commit/b55a46ae99c32068ed306a5ecdaafa9f75504cd7), [Commit](https://github.com/open-webui/open-webui/commit/2bb13d5dbc6e233856e8aa26143222ceda8f6c11)
|
||||
- 🔁 Users can now regenerate messages without crashes when the parent message is missing or corrupted in the chat history. [#20264](https://github.com/open-webui/open-webui/pull/20264)
|
||||
- ✏️ Users no longer experience TipTap rich text editor crashes with "editor view is not available" errors when plugins or async methods try to access the editor after it has been destroyed. [#20266](https://github.com/open-webui/open-webui/pull/20266)
|
||||
- 📗 Administrators with bypass access control enabled now correctly have write access to all knowledge bases. [#20371](https://github.com/open-webui/open-webui/pull/20371)
|
||||
- 🔍 Administrators using local CrossEncoder reranking models now see proper relevance threshold behavior through MS MARCO model score normalization to the 0-1 range via sigmoid activation. [#19999](https://github.com/open-webui/open-webui/issues/19999), [#20228](https://github.com/open-webui/open-webui/pull/20228)
|
||||
- 🎯 Administrators using local SentenceTransformers embedding engine now benefit from proper batch size settings, preventing excessive memory usage from the default batch size of 32. [#20053](https://github.com/open-webui/open-webui/issues/20053), [#20054](https://github.com/open-webui/open-webui/pull/20054), [Commit](https://github.com/open-webui/open-webui/commit/e4a5b06ca68303512678b4d2dc296bc78b9f983f)
|
||||
- 🔧 Administrators and users in offline mode or restricted environments like uv, poetry, and NixOS no longer experience crashes when Tools and Functions have frontmatter requirements, as pip installation is now skipped when offline mode is enabled. [#20320](https://github.com/open-webui/open-webui/issues/20320), [#20321](https://github.com/open-webui/open-webui/pull/20321), [Commit](https://github.com/open-webui/open-webui/commit/bd07ef8)
|
||||
- 📄 Administrators can now properly configure the MinerU document parsing service as the MinerU Cloud API key field is now available in the Admin Panel Documents settings. [#20319](https://github.com/open-webui/open-webui/issues/20319), [#20328](https://github.com/open-webui/open-webui/pull/20328)
|
||||
- ⚠️ Administrators no longer see SyntaxWarnings for invalid escape sequences in password validation regex patterns. [#20298](https://github.com/open-webui/open-webui/pull/20298), [Commit](https://github.com/open-webui/open-webui/commit/e55bf2c2ac391caed871d41f0484820091081908)
|
||||
- 🎨 Users with ComfyUI workflows now see only the intended final output images in chat instead of duplicate images from intermediate processing nodes like masks, crops, or segmentation previews. [#20158](https://github.com/open-webui/open-webui/issues/20158), [#20182](https://github.com/open-webui/open-webui/pull/20182)
|
||||
- 🖼️ Users with image generation enabled no longer see false vision capability warnings, allowing them to send follow-up messages after generating images and to send images to non-vision models for image editing. [#20129](https://github.com/open-webui/open-webui/issues/20129), [#20256](https://github.com/open-webui/open-webui/pull/20256)
|
||||
- 🔌 Administrators no longer experience infinite loading screens when invalid or MCP-style configurations are used with OpenAPI connection types for external tools. [#20207](https://github.com/open-webui/open-webui/issues/20207), [#20257](https://github.com/open-webui/open-webui/pull/20257)
|
||||
- 📥 Administrators no longer encounter TypeError crashes during SHA256 verification when uploading GGUF models via URL, fixing 500 Internal Server Error crashes. [#20263](https://github.com/open-webui/open-webui/issues/20263)
|
||||
- 🚦 Users with Brave Search now experience automatic retry with a 1-second delay when hitting rate limits, preventing failures when sequential requests exceed the 1 request per second limit, though this only works reliably when web search concurrency is set to a maximum of 1. [#15134](https://github.com/open-webui/open-webui/issues/15134), [#20255](https://github.com/open-webui/open-webui/pull/20255)
|
||||
- 🗄️ Administrators with Redis Sentinel deployments no longer experience crashes during websocket disconnections due to improper async-generator handling in the YDocManager. [#20142](https://github.com/open-webui/open-webui/issues/20142), [#20145](https://github.com/open-webui/open-webui/pull/20145)
|
||||
- 🔐 Administrators using SCIM group management no longer encounter 500 errors when working with groups that have no members. [#20187](https://github.com/open-webui/open-webui/pull/20187)
|
||||
- 🔗 Users now experience more reliable citations from AI models, especially when using smaller or weaker models that may not format citation references perfectly. [Commit](https://github.com/open-webui/open-webui/commit/c0ec04935b4eea3d334bfdec2fc41278f1085a49)
|
||||
- 🕸️ Administrators can now successfully save WebSearch settings without encountering validation errors for domain filter lists, YouTube language settings, or timeout values. [#20422](https://github.com/open-webui/open-webui/pull/20422)
|
||||
- 📦 Administrators installing with the uv package manager now experience successful installation after deprecated dependencies that were causing conflicts were removed. [#20177](https://github.com/open-webui/open-webui/issues/20177), [#20192](https://github.com/open-webui/open-webui/pull/20192)
|
||||
- ⏱️ Administrators using custom "AIOHTTP_CLIENT_TIMEOUT" settings now see the configured timeout correctly applied to embedding generation, OAuth discovery, webhook calls, and tool/function loading instead of falling back to the default 300-second timeout. [Commit](https://github.com/open-webui/open-webui/commit/e67891a374625d9888ec391da561f0b4ed79ed5d)
|
||||
|
||||
### Changed
|
||||
|
||||
- ⚠️ This release includes a major overhaul of database connection handling in the backend that requires all instances in multi-worker, multi-server, or load-balanced deployments to be updated simultaneously; running mixed versions will cause failures due to incompatible database connection management between old and new instances.
|
||||
- 📝 Administrators who previously used the standalone "Markdown (Header)" text splitter must now switch to "character" or "token" mode with the new "ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER" toggle enabled, as document chunking now applies markdown header splitting as a preprocessing step before character or token splitting. [Commit](https://github.com/open-webui/open-webui/commit/d3a682759f375c6cb0bc8c170f62863a070f712e), [Commit](https://github.com/open-webui/open-webui/commit/18a33a079bf07487edffc421a721c86194cc90c9), [Commit](https://github.com/open-webui/open-webui/commit/08bf4670ec862018f9dc57296cb19fd5eab14ef6)
|
||||
- 🖼️ Users no longer see the "Generate Image" action button in chat message interfaces; custom function should be used. [Commit](https://github.com/open-webui/open-webui/commit/f0829ba6e6fd200702fb76efc43dd785cf87fec9)
|
||||
- 🔗 Administrators will find the Admin Evaluations page at the new URL "/admin/evaluations/feedback" instead of "/admin/evaluations/feedbacks" to use the correct uncountable form of the word. [#20296](https://github.com/open-webui/open-webui/pull/20296)
|
||||
- 🔐 Scripts or integrations that directly called Image Generation, Web Search, or Audio APIs while those features were disabled in the Admin UI will now receive 403 Forbidden errors, as backend permission enforcement has been added to match frontend restrictions. [#20471](https://github.com/open-webui/open-webui/pull/20471)
|
||||
- 👥 The default group sharing permission changed from "Members" to "Anyone", meaning users can now share content to any group configured with "Anyone" permission regardless of their membership in that group. [Commit](https://github.com/open-webui/open-webui/commit/ca514cd3eda2524b8da472ef17c0ccb216bac2e8)
|
||||
|
||||
## [0.6.43] - 2025-12-22
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
10
Dockerfile
10
Dockerfile
|
|
@ -13,6 +13,7 @@ ARG USE_CUDA_VER=cu128
|
|||
# IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
|
||||
ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
||||
ARG USE_RERANKING_MODEL=""
|
||||
ARG USE_AUXILIARY_EMBEDDING_MODEL=TaylorAI/bge-micro-v2
|
||||
|
||||
# Tiktoken encoding name; models to use can be found at https://huggingface.co/models?library=tiktoken
|
||||
ARG USE_TIKTOKEN_ENCODING_NAME="cl100k_base"
|
||||
|
|
@ -42,7 +43,7 @@ ENV APP_BUILD_HASH=${BUILD_HASH}
|
|||
RUN npm run build
|
||||
|
||||
######## WebUI backend ########
|
||||
FROM python:3.11-slim-bookworm AS base
|
||||
FROM python:3.11.14-slim-bookworm AS base
|
||||
|
||||
# Use args
|
||||
ARG USE_CUDA
|
||||
|
|
@ -52,6 +53,7 @@ ARG USE_SLIM
|
|||
ARG USE_PERMISSION_HARDENING
|
||||
ARG USE_EMBEDDING_MODEL
|
||||
ARG USE_RERANKING_MODEL
|
||||
ARG USE_AUXILIARY_EMBEDDING_MODEL
|
||||
ARG UID
|
||||
ARG GID
|
||||
|
||||
|
|
@ -67,7 +69,8 @@ ENV ENV=prod \
|
|||
USE_SLIM_DOCKER=${USE_SLIM} \
|
||||
USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
|
||||
USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} \
|
||||
USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL}
|
||||
USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL} \
|
||||
USE_AUXILIARY_EMBEDDING_MODEL_DOCKER=${USE_AUXILIARY_EMBEDDING_MODEL}
|
||||
|
||||
## Basis URL Config ##
|
||||
ENV OLLAMA_BASE_URL="/ollama" \
|
||||
|
|
@ -88,6 +91,7 @@ ENV WHISPER_MODEL="base" \
|
|||
## RAG Embedding model settings ##
|
||||
ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
|
||||
RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \
|
||||
AUXILIARY_EMBEDDING_MODEL="$USE_AUXILIARY_EMBEDDING_MODEL_DOCKER" \
|
||||
SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
|
||||
|
||||
## Tiktoken model settings ##
|
||||
|
|
@ -136,6 +140,7 @@ RUN pip3 install --no-cache-dir uv && \
|
|||
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
|
||||
uv pip install --system -r requirements.txt --no-cache-dir && \
|
||||
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')" && \
|
||||
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
|
||||
python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
|
||||
else \
|
||||
|
|
@ -143,6 +148,7 @@ RUN pip3 install --no-cache-dir uv && \
|
|||
uv pip install --system -r requirements.txt --no-cache-dir && \
|
||||
if [ "$USE_SLIM" != "true" ]; then \
|
||||
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')" && \
|
||||
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
|
||||
python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
|
||||
fi; \
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from authlib.integrations.starlette_client import OAuth
|
|||
from open_webui.env import (
|
||||
DATA_DIR,
|
||||
DATABASE_URL,
|
||||
ENABLE_DB_MIGRATIONS,
|
||||
ENV,
|
||||
REDIS_URL,
|
||||
REDIS_KEY_PREFIX,
|
||||
|
|
@ -67,7 +68,8 @@ def run_migrations():
|
|||
log.exception(f"Error running migrations: {e}")
|
||||
|
||||
|
||||
run_migrations()
|
||||
if ENABLE_DB_MIGRATIONS:
|
||||
run_migrations()
|
||||
|
||||
|
||||
class Config(Base):
|
||||
|
|
@ -1468,6 +1470,15 @@ USER_PERMISSIONS_FEATURES_API_KEYS = (
|
|||
os.environ.get("USER_PERMISSIONS_FEATURES_API_KEYS", "False").lower() == "true"
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_FEATURES_MEMORIES = (
|
||||
os.environ.get("USER_PERMISSIONS_FEATURES_MEMORIES", "True").lower() == "true"
|
||||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_SETTINGS_INTERFACE = (
|
||||
os.environ.get("USER_PERMISSIONS_SETTINGS_INTERFACE", "True").lower() == "true"
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_USER_PERMISSIONS = {
|
||||
"workspace": {
|
||||
|
|
@ -1526,6 +1537,10 @@ DEFAULT_USER_PERMISSIONS = {
|
|||
"web_search": USER_PERMISSIONS_FEATURES_WEB_SEARCH,
|
||||
"image_generation": USER_PERMISSIONS_FEATURES_IMAGE_GENERATION,
|
||||
"code_interpreter": USER_PERMISSIONS_FEATURES_CODE_INTERPRETER,
|
||||
"memories": USER_PERMISSIONS_FEATURES_MEMORIES,
|
||||
},
|
||||
"settings": {
|
||||
"interface": USER_PERMISSIONS_SETTINGS_INTERFACE,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -1541,6 +1556,12 @@ ENABLE_FOLDERS = PersistentConfig(
|
|||
os.environ.get("ENABLE_FOLDERS", "True").lower() == "true",
|
||||
)
|
||||
|
||||
FOLDER_MAX_FILE_COUNT = PersistentConfig(
|
||||
"FOLDER_MAX_FILE_COUNT",
|
||||
"folders.max_file_count",
|
||||
os.environ.get("FOLDER_MAX_FILE_COUNT", ""),
|
||||
)
|
||||
|
||||
ENABLE_CHANNELS = PersistentConfig(
|
||||
"ENABLE_CHANNELS",
|
||||
"channels.enable",
|
||||
|
|
@ -1553,6 +1574,12 @@ ENABLE_NOTES = PersistentConfig(
|
|||
os.environ.get("ENABLE_NOTES", "True").lower() == "true",
|
||||
)
|
||||
|
||||
ENABLE_USER_STATUS = PersistentConfig(
|
||||
"ENABLE_USER_STATUS",
|
||||
"users.enable_status",
|
||||
os.environ.get("ENABLE_USER_STATUS", "True").lower() == "true",
|
||||
)
|
||||
|
||||
ENABLE_EVALUATION_ARENA_MODELS = PersistentConfig(
|
||||
"ENABLE_EVALUATION_ARENA_MODELS",
|
||||
"evaluation.arena.enable",
|
||||
|
|
@ -2067,6 +2094,12 @@ ENABLE_CODE_INTERPRETER = PersistentConfig(
|
|||
os.environ.get("ENABLE_CODE_INTERPRETER", "True").lower() == "true",
|
||||
)
|
||||
|
||||
ENABLE_MEMORIES = PersistentConfig(
|
||||
"ENABLE_MEMORIES",
|
||||
"memories.enable",
|
||||
os.environ.get("ENABLE_MEMORIES", "True").lower() == "true",
|
||||
)
|
||||
|
||||
CODE_INTERPRETER_ENGINE = PersistentConfig(
|
||||
"CODE_INTERPRETER_ENGINE",
|
||||
"code_interpreter.engine",
|
||||
|
|
@ -2340,6 +2373,51 @@ else:
|
|||
except Exception:
|
||||
PGVECTOR_IVFFLAT_LISTS = 100
|
||||
|
||||
# openGauss
|
||||
OPENGAUSS_DB_URL = os.environ.get("OPENGAUSS_DB_URL", DATABASE_URL)
|
||||
|
||||
OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH = int(
|
||||
os.environ.get("OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH", "1536")
|
||||
)
|
||||
|
||||
OPENGAUSS_POOL_SIZE = os.environ.get("OPENGAUSS_POOL_SIZE", None)
|
||||
|
||||
if OPENGAUSS_POOL_SIZE != None:
|
||||
try:
|
||||
OPENGAUSS_POOL_SIZE = int(OPENGAUSS_POOL_SIZE)
|
||||
except Exception:
|
||||
OPENGAUSS_POOL_SIZE = None
|
||||
|
||||
OPENGAUSS_POOL_MAX_OVERFLOW = os.environ.get("OPENGAUSS_POOL_MAX_OVERFLOW", 0)
|
||||
|
||||
if OPENGAUSS_POOL_MAX_OVERFLOW == "":
|
||||
OPENGAUSS_POOL_MAX_OVERFLOW = 0
|
||||
else:
|
||||
try:
|
||||
OPENGAUSS_POOL_MAX_OVERFLOW = int(OPENGAUSS_POOL_MAX_OVERFLOW)
|
||||
except Exception:
|
||||
OPENGAUSS_POOL_MAX_OVERFLOW = 0
|
||||
|
||||
OPENGAUSS_POOL_TIMEOUT = os.environ.get("OPENGAUSS_POOL_TIMEOUT", 30)
|
||||
|
||||
if OPENGAUSS_POOL_TIMEOUT == "":
|
||||
OPENGAUSS_POOL_TIMEOUT = 30
|
||||
else:
|
||||
try:
|
||||
OPENGAUSS_POOL_TIMEOUT = int(OPENGAUSS_POOL_TIMEOUT)
|
||||
except Exception:
|
||||
OPENGAUSS_POOL_TIMEOUT = 30
|
||||
|
||||
OPENGAUSS_POOL_RECYCLE = os.environ.get("OPENGAUSS_POOL_RECYCLE", 3600)
|
||||
|
||||
if OPENGAUSS_POOL_RECYCLE == "":
|
||||
OPENGAUSS_POOL_RECYCLE = 3600
|
||||
else:
|
||||
try:
|
||||
OPENGAUSS_POOL_RECYCLE = int(OPENGAUSS_POOL_RECYCLE)
|
||||
except Exception:
|
||||
OPENGAUSS_POOL_RECYCLE = 3600
|
||||
|
||||
# Pinecone
|
||||
PINECONE_API_KEY = os.environ.get("PINECONE_API_KEY", None)
|
||||
PINECONE_ENVIRONMENT = os.environ.get("PINECONE_ENVIRONMENT", None)
|
||||
|
|
@ -2815,6 +2893,12 @@ RAG_TEXT_SPLITTER = PersistentConfig(
|
|||
os.environ.get("RAG_TEXT_SPLITTER", ""),
|
||||
)
|
||||
|
||||
ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER = PersistentConfig(
|
||||
"ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER",
|
||||
"rag.enable_markdown_header_text_splitter",
|
||||
os.environ.get("ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER", "True").lower() == "true",
|
||||
)
|
||||
|
||||
|
||||
TIKTOKEN_CACHE_DIR = os.environ.get("TIKTOKEN_CACHE_DIR", f"{CACHE_DIR}/tiktoken")
|
||||
TIKTOKEN_ENCODING_NAME = PersistentConfig(
|
||||
|
|
@ -2827,6 +2911,13 @@ TIKTOKEN_ENCODING_NAME = PersistentConfig(
|
|||
CHUNK_SIZE = PersistentConfig(
|
||||
"CHUNK_SIZE", "rag.chunk_size", int(os.environ.get("CHUNK_SIZE", "1000"))
|
||||
)
|
||||
|
||||
CHUNK_MIN_SIZE_TARGET = PersistentConfig(
|
||||
"CHUNK_MIN_SIZE_TARGET",
|
||||
"rag.chunk_min_size_target",
|
||||
int(os.environ.get("CHUNK_MIN_SIZE_TARGET", "0")),
|
||||
)
|
||||
|
||||
CHUNK_OVERLAP = PersistentConfig(
|
||||
"CHUNK_OVERLAP",
|
||||
"rag.chunk_overlap",
|
||||
|
|
@ -3127,12 +3218,24 @@ SERPLY_API_KEY = PersistentConfig(
|
|||
os.getenv("SERPLY_API_KEY", ""),
|
||||
)
|
||||
|
||||
DDGS_BACKEND = PersistentConfig(
|
||||
"DDGS_BACKEND",
|
||||
"rag.web.search.ddgs_backend",
|
||||
os.getenv("DDGS_BACKEND", "auto"),
|
||||
)
|
||||
|
||||
JINA_API_KEY = PersistentConfig(
|
||||
"JINA_API_KEY",
|
||||
"rag.web.search.jina_api_key",
|
||||
os.getenv("JINA_API_KEY", ""),
|
||||
)
|
||||
|
||||
JINA_API_BASE_URL = PersistentConfig(
|
||||
"JINA_API_BASE_URL",
|
||||
"rag.web.search.jina_api_base_url",
|
||||
os.getenv("JINA_API_BASE_URL", ""),
|
||||
)
|
||||
|
||||
SEARCHAPI_API_KEY = PersistentConfig(
|
||||
"SEARCHAPI_API_KEY",
|
||||
"rag.web.search.searchapi_api_key",
|
||||
|
|
@ -3267,6 +3370,12 @@ FIRECRAWL_API_BASE_URL = PersistentConfig(
|
|||
os.environ.get("FIRECRAWL_API_BASE_URL", "https://api.firecrawl.dev"),
|
||||
)
|
||||
|
||||
FIRECRAWL_TIMEOUT = PersistentConfig(
|
||||
"FIRECRAWL_TIMEOUT",
|
||||
"rag.web.loader.firecrawl_timeout",
|
||||
os.environ.get("FIRECRAWL_TIMEOUT", ""),
|
||||
)
|
||||
|
||||
EXTERNAL_WEB_SEARCH_URL = PersistentConfig(
|
||||
"EXTERNAL_WEB_SEARCH_URL",
|
||||
"rag.web.search.external_web_search_url",
|
||||
|
|
@ -3628,17 +3737,16 @@ WHISPER_MODEL = PersistentConfig(
|
|||
os.getenv("WHISPER_MODEL", "base"),
|
||||
)
|
||||
|
||||
WHISPER_COMPUTE_TYPE = os.getenv("WHISPER_COMPUTE_TYPE", "int8")
|
||||
WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models")
|
||||
WHISPER_MODEL_AUTO_UPDATE = (
|
||||
not OFFLINE_MODE
|
||||
and os.environ.get("WHISPER_MODEL_AUTO_UPDATE", "").lower() == "true"
|
||||
)
|
||||
|
||||
WHISPER_VAD_FILTER = PersistentConfig(
|
||||
"WHISPER_VAD_FILTER",
|
||||
"audio.stt.whisper_vad_filter",
|
||||
os.getenv("WHISPER_VAD_FILTER", "False").lower() == "true",
|
||||
)
|
||||
WHISPER_VAD_FILTER = os.getenv("WHISPER_VAD_FILTER", "False").lower() == "true"
|
||||
|
||||
WHISPER_MULTILINGUAL = os.getenv("WHISPER_MULTILINGUAL", "False").lower() == "true"
|
||||
|
||||
WHISPER_LANGUAGE = os.getenv("WHISPER_LANGUAGE", "").lower() or None
|
||||
|
||||
|
|
|
|||
|
|
@ -117,6 +117,8 @@ VERSION = PACKAGE_DATA["version"]
|
|||
DEPLOYMENT_ID = os.environ.get("DEPLOYMENT_ID", "")
|
||||
INSTANCE_ID = os.environ.get("INSTANCE_ID", str(uuid4()))
|
||||
|
||||
ENABLE_DB_MIGRATIONS = os.environ.get("ENABLE_DB_MIGRATIONS", "True").lower() == "true"
|
||||
|
||||
|
||||
# Function to parse each section
|
||||
def parse_section(section):
|
||||
|
|
@ -354,6 +356,8 @@ ENABLE_REALTIME_CHAT_SAVE = (
|
|||
|
||||
ENABLE_QUERIES_CACHE = os.environ.get("ENABLE_QUERIES_CACHE", "False").lower() == "true"
|
||||
|
||||
RAG_SYSTEM_CONTEXT = os.environ.get("RAG_SYSTEM_CONTEXT", "False").lower() == "true"
|
||||
|
||||
####################################
|
||||
# REDIS
|
||||
####################################
|
||||
|
|
@ -409,6 +413,16 @@ ENABLE_SIGNUP_PASSWORD_CONFIRMATION = (
|
|||
os.environ.get("ENABLE_SIGNUP_PASSWORD_CONFIRMATION", "False").lower() == "true"
|
||||
)
|
||||
|
||||
####################################
|
||||
# Admin Account Runtime Creation
|
||||
####################################
|
||||
|
||||
# Optional env vars for creating an admin account on startup
|
||||
# Useful for headless/automated deployments
|
||||
WEBUI_ADMIN_EMAIL = os.environ.get("WEBUI_ADMIN_EMAIL", "")
|
||||
WEBUI_ADMIN_PASSWORD = os.environ.get("WEBUI_ADMIN_PASSWORD", "")
|
||||
WEBUI_ADMIN_NAME = os.environ.get("WEBUI_ADMIN_NAME", "Admin")
|
||||
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
|
||||
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
|
||||
)
|
||||
|
|
@ -426,12 +440,14 @@ PASSWORD_VALIDATION_REGEX_PATTERN = os.environ.get(
|
|||
"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$",
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
PASSWORD_VALIDATION_REGEX_PATTERN = rf"{PASSWORD_VALIDATION_REGEX_PATTERN}"
|
||||
PASSWORD_VALIDATION_REGEX_PATTERN = re.compile(PASSWORD_VALIDATION_REGEX_PATTERN)
|
||||
except Exception as e:
|
||||
log.error(f"Invalid PASSWORD_VALIDATION_REGEX_PATTERN: {e}")
|
||||
PASSWORD_VALIDATION_REGEX_PATTERN = re.compile(
|
||||
"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$"
|
||||
r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -667,6 +683,8 @@ except ValueError:
|
|||
WEBSOCKET_SERVER_PING_INTERVAL = 25
|
||||
|
||||
|
||||
REQUESTS_VERIFY = os.environ.get("REQUESTS_VERIFY", "True").lower() == "true"
|
||||
|
||||
AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
|
||||
|
||||
if AIOHTTP_CLIENT_TIMEOUT == "":
|
||||
|
|
@ -760,6 +778,16 @@ else:
|
|||
except Exception:
|
||||
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None
|
||||
|
||||
# Whether to apply sigmoid normalization to CrossEncoder reranking scores.
|
||||
# When enabled (default), scores are normalized to 0-1 range for proper
|
||||
# relevance threshold behavior with MS MARCO models.
|
||||
SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION = (
|
||||
os.environ.get(
|
||||
"SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION", "True"
|
||||
).lower()
|
||||
== "true"
|
||||
)
|
||||
|
||||
####################################
|
||||
# OFFLINE_MODE
|
||||
####################################
|
||||
|
|
@ -776,6 +804,11 @@ if OFFLINE_MODE:
|
|||
####################################
|
||||
# AUDIT LOGGING
|
||||
####################################
|
||||
|
||||
|
||||
ENABLE_AUDIT_STDOUT = os.getenv("ENABLE_AUDIT_STDOUT", "False").lower() == "true"
|
||||
ENABLE_AUDIT_LOGS_FILE = os.getenv("ENABLE_AUDIT_LOGS_FILE", "True").lower() == "true"
|
||||
|
||||
# Where to store log file
|
||||
# Defaults to the DATA_DIR/audit.log. To set AUDIT_LOGS_FILE_PATH you need to
|
||||
# provide the whole path, like: /app/audit.log
|
||||
|
|
|
|||
|
|
@ -14,11 +14,12 @@ from open_webui.env import (
|
|||
DATABASE_POOL_SIZE,
|
||||
DATABASE_POOL_TIMEOUT,
|
||||
DATABASE_ENABLE_SQLITE_WAL,
|
||||
ENABLE_DB_MIGRATIONS,
|
||||
)
|
||||
from peewee_migrate import Router
|
||||
from sqlalchemy import Dialect, create_engine, MetaData, event, types
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker, Session
|
||||
from sqlalchemy.pool import QueuePool, NullPool
|
||||
from sqlalchemy.sql.type_api import _T
|
||||
from typing_extensions import Self
|
||||
|
|
@ -75,7 +76,8 @@ def handle_peewee_migration(DATABASE_URL):
|
|||
assert db.is_closed(), "Database connection is still open."
|
||||
|
||||
|
||||
handle_peewee_migration(DATABASE_URL)
|
||||
if ENABLE_DB_MIGRATIONS:
|
||||
handle_peewee_migration(DATABASE_URL)
|
||||
|
||||
|
||||
SQLALCHEMY_DATABASE_URL = DATABASE_URL
|
||||
|
|
@ -146,7 +148,7 @@ SessionLocal = sessionmaker(
|
|||
)
|
||||
metadata_obj = MetaData(schema=DATABASE_SCHEMA)
|
||||
Base = declarative_base(metadata=metadata_obj)
|
||||
Session = scoped_session(SessionLocal)
|
||||
ScopedSession = scoped_session(SessionLocal)
|
||||
|
||||
|
||||
def get_session():
|
||||
|
|
@ -158,3 +160,12 @@ def get_session():
|
|||
|
||||
|
||||
get_db = contextmanager(get_session)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_db_context(db: Optional[Session] = None):
|
||||
if isinstance(db, Session):
|
||||
yield db
|
||||
else:
|
||||
with get_db() as session:
|
||||
yield session
|
||||
|
|
|
|||
|
|
@ -102,7 +102,9 @@ from open_webui.routers.retrieval import (
|
|||
get_rf,
|
||||
)
|
||||
|
||||
from open_webui.internal.db import Session, engine
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import ScopedSession, engine, get_session
|
||||
|
||||
from open_webui.models.functions import Functions
|
||||
from open_webui.models.models import Models
|
||||
|
|
@ -143,6 +145,7 @@ from open_webui.config import (
|
|||
CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
|
||||
CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
|
||||
CODE_INTERPRETER_JUPYTER_TIMEOUT,
|
||||
ENABLE_MEMORIES,
|
||||
# Image
|
||||
AUTOMATIC1111_API_AUTH,
|
||||
AUTOMATIC1111_BASE_URL,
|
||||
|
|
@ -206,6 +209,7 @@ from open_webui.config import (
|
|||
PLAYWRIGHT_TIMEOUT,
|
||||
FIRECRAWL_API_BASE_URL,
|
||||
FIRECRAWL_API_KEY,
|
||||
FIRECRAWL_TIMEOUT,
|
||||
WEB_LOADER_ENGINE,
|
||||
WEB_LOADER_CONCURRENT_REQUESTS,
|
||||
WEB_LOADER_TIMEOUT,
|
||||
|
|
@ -250,6 +254,7 @@ from open_webui.config import (
|
|||
RAG_OLLAMA_BASE_URL,
|
||||
RAG_OLLAMA_API_KEY,
|
||||
CHUNK_OVERLAP,
|
||||
CHUNK_MIN_SIZE_TARGET,
|
||||
CHUNK_SIZE,
|
||||
CONTENT_EXTRACTION_ENGINE,
|
||||
DATALAB_MARKER_API_KEY,
|
||||
|
|
@ -280,6 +285,7 @@ from open_webui.config import (
|
|||
MISTRAL_OCR_API_BASE_URL,
|
||||
MISTRAL_OCR_API_KEY,
|
||||
RAG_TEXT_SPLITTER,
|
||||
ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER,
|
||||
TIKTOKEN_ENCODING_NAME,
|
||||
PDF_EXTRACT_IMAGES,
|
||||
YOUTUBE_LOADER_LANGUAGE,
|
||||
|
|
@ -295,6 +301,7 @@ from open_webui.config import (
|
|||
WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||
OLLAMA_CLOUD_WEB_SEARCH_API_KEY,
|
||||
JINA_API_KEY,
|
||||
JINA_API_BASE_URL,
|
||||
SEARCHAPI_API_KEY,
|
||||
SEARCHAPI_ENGINE,
|
||||
SERPAPI_API_KEY,
|
||||
|
|
@ -306,6 +313,7 @@ from open_webui.config import (
|
|||
YACY_PASSWORD,
|
||||
SERPER_API_KEY,
|
||||
SERPLY_API_KEY,
|
||||
DDGS_BACKEND,
|
||||
SERPSTACK_API_KEY,
|
||||
SERPSTACK_HTTPS,
|
||||
TAVILY_API_KEY,
|
||||
|
|
@ -358,8 +366,10 @@ from open_webui.config import (
|
|||
ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS,
|
||||
API_KEYS_ALLOWED_ENDPOINTS,
|
||||
ENABLE_FOLDERS,
|
||||
FOLDER_MAX_FILE_COUNT,
|
||||
ENABLE_CHANNELS,
|
||||
ENABLE_NOTES,
|
||||
ENABLE_USER_STATUS,
|
||||
ENABLE_COMMUNITY_SHARING,
|
||||
ENABLE_MESSAGE_RATING,
|
||||
ENABLE_USER_WEBHOOKS,
|
||||
|
|
@ -476,6 +486,10 @@ from open_webui.env import (
|
|||
AIOHTTP_CLIENT_SESSION_SSL,
|
||||
ENABLE_STAR_SESSIONS_MIDDLEWARE,
|
||||
ENABLE_PUBLIC_ACTIVE_USERS_COUNT,
|
||||
# Admin Account Runtime Creation
|
||||
WEBUI_ADMIN_EMAIL,
|
||||
WEBUI_ADMIN_PASSWORD,
|
||||
WEBUI_ADMIN_NAME,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -500,6 +514,7 @@ from open_webui.utils.auth import (
|
|||
decode_token,
|
||||
get_admin_user,
|
||||
get_verified_user,
|
||||
create_admin_user,
|
||||
)
|
||||
from open_webui.utils.plugin import install_tool_and_function_dependencies
|
||||
from open_webui.utils.oauth import (
|
||||
|
|
@ -578,6 +593,12 @@ async def lifespan(app: FastAPI):
|
|||
if LICENSE_KEY:
|
||||
get_license_data(app, LICENSE_KEY)
|
||||
|
||||
# Create admin account from env vars if specified and no users exist
|
||||
if WEBUI_ADMIN_EMAIL and WEBUI_ADMIN_PASSWORD:
|
||||
if create_admin_user(WEBUI_ADMIN_EMAIL, WEBUI_ADMIN_PASSWORD, WEBUI_ADMIN_NAME):
|
||||
# Disable signup since we now have an admin
|
||||
app.state.config.ENABLE_SIGNUP = False
|
||||
|
||||
# This should be blocking (sync) so functions are not deactivated on first /get_models calls
|
||||
# when the first user lands on the / route.
|
||||
log.info("Installing external dependencies of functions and tools...")
|
||||
|
|
@ -774,11 +795,13 @@ app.state.config.BANNERS = WEBUI_BANNERS
|
|||
|
||||
|
||||
app.state.config.ENABLE_FOLDERS = ENABLE_FOLDERS
|
||||
app.state.config.FOLDER_MAX_FILE_COUNT = FOLDER_MAX_FILE_COUNT
|
||||
app.state.config.ENABLE_CHANNELS = ENABLE_CHANNELS
|
||||
app.state.config.ENABLE_NOTES = ENABLE_NOTES
|
||||
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
|
||||
app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING
|
||||
app.state.config.ENABLE_USER_WEBHOOKS = ENABLE_USER_WEBHOOKS
|
||||
app.state.config.ENABLE_USER_STATUS = ENABLE_USER_STATUS
|
||||
|
||||
app.state.config.ENABLE_EVALUATION_ARENA_MODELS = ENABLE_EVALUATION_ARENA_MODELS
|
||||
app.state.config.EVALUATION_ARENA_MODELS = EVALUATION_ARENA_MODELS
|
||||
|
|
@ -886,11 +909,17 @@ app.state.config.MINERU_API_TIMEOUT = MINERU_API_TIMEOUT
|
|||
app.state.config.MINERU_PARAMS = MINERU_PARAMS
|
||||
|
||||
app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER
|
||||
app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER = (
|
||||
ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER
|
||||
)
|
||||
|
||||
app.state.config.TIKTOKEN_ENCODING_NAME = TIKTOKEN_ENCODING_NAME
|
||||
|
||||
app.state.config.CHUNK_SIZE = CHUNK_SIZE
|
||||
app.state.config.CHUNK_MIN_SIZE_TARGET = CHUNK_MIN_SIZE_TARGET
|
||||
app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
|
||||
|
||||
|
||||
app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
|
||||
app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
||||
app.state.config.RAG_EMBEDDING_BATCH_SIZE = RAG_EMBEDDING_BATCH_SIZE
|
||||
|
|
@ -955,12 +984,14 @@ app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY
|
|||
app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS
|
||||
app.state.config.SERPER_API_KEY = SERPER_API_KEY
|
||||
app.state.config.SERPLY_API_KEY = SERPLY_API_KEY
|
||||
app.state.config.DDGS_BACKEND = DDGS_BACKEND
|
||||
app.state.config.TAVILY_API_KEY = TAVILY_API_KEY
|
||||
app.state.config.SEARCHAPI_API_KEY = SEARCHAPI_API_KEY
|
||||
app.state.config.SEARCHAPI_ENGINE = SEARCHAPI_ENGINE
|
||||
app.state.config.SERPAPI_API_KEY = SERPAPI_API_KEY
|
||||
app.state.config.SERPAPI_ENGINE = SERPAPI_ENGINE
|
||||
app.state.config.JINA_API_KEY = JINA_API_KEY
|
||||
app.state.config.JINA_API_BASE_URL = JINA_API_BASE_URL
|
||||
app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT
|
||||
app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY
|
||||
app.state.config.EXA_API_KEY = EXA_API_KEY
|
||||
|
|
@ -980,6 +1011,7 @@ app.state.config.PLAYWRIGHT_WS_URL = PLAYWRIGHT_WS_URL
|
|||
app.state.config.PLAYWRIGHT_TIMEOUT = PLAYWRIGHT_TIMEOUT
|
||||
app.state.config.FIRECRAWL_API_BASE_URL = FIRECRAWL_API_BASE_URL
|
||||
app.state.config.FIRECRAWL_API_KEY = FIRECRAWL_API_KEY
|
||||
app.state.config.FIRECRAWL_TIMEOUT = FIRECRAWL_TIMEOUT
|
||||
app.state.config.TAVILY_EXTRACT_DEPTH = TAVILY_EXTRACT_DEPTH
|
||||
|
||||
app.state.EMBEDDING_FUNCTION = None
|
||||
|
|
@ -1088,6 +1120,7 @@ app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = CODE_INTERPRETER_JUPYTER_TIM
|
|||
app.state.config.IMAGE_GENERATION_ENGINE = IMAGE_GENERATION_ENGINE
|
||||
app.state.config.ENABLE_IMAGE_GENERATION = ENABLE_IMAGE_GENERATION
|
||||
app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = ENABLE_IMAGE_PROMPT_GENERATION
|
||||
app.state.config.ENABLE_MEMORIES = ENABLE_MEMORIES
|
||||
|
||||
app.state.config.IMAGE_GENERATION_MODEL = IMAGE_GENERATION_MODEL
|
||||
app.state.config.IMAGE_SIZE = IMAGE_SIZE
|
||||
|
|
@ -1141,7 +1174,6 @@ app.state.config.STT_OPENAI_API_BASE_URL = AUDIO_STT_OPENAI_API_BASE_URL
|
|||
app.state.config.STT_OPENAI_API_KEY = AUDIO_STT_OPENAI_API_KEY
|
||||
|
||||
app.state.config.WHISPER_MODEL = WHISPER_MODEL
|
||||
app.state.config.WHISPER_VAD_FILTER = WHISPER_VAD_FILTER
|
||||
app.state.config.DEEPGRAM_API_KEY = DEEPGRAM_API_KEY
|
||||
|
||||
app.state.config.AUDIO_STT_AZURE_API_KEY = AUDIO_STT_AZURE_API_KEY
|
||||
|
|
@ -1324,7 +1356,7 @@ app.add_middleware(APIKeyRestrictionMiddleware)
|
|||
async def commit_session_after_request(request: Request, call_next):
|
||||
response = await call_next(request)
|
||||
# log.debug("Commit session after request")
|
||||
Session.commit()
|
||||
ScopedSession.commit()
|
||||
return response
|
||||
|
||||
|
||||
|
|
@ -1645,7 +1677,7 @@ async def chat_completion(
|
|||
)
|
||||
|
||||
# Insert chat files from parent message if any
|
||||
parent_message = metadata.get("parent_message", {})
|
||||
parent_message = metadata.get("parent_message") or {}
|
||||
parent_message_files = parent_message.get("files", [])
|
||||
if parent_message_files:
|
||||
try:
|
||||
|
|
@ -1902,6 +1934,7 @@ async def get_app_config(request: Request):
|
|||
{
|
||||
"enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS,
|
||||
"enable_folders": app.state.config.ENABLE_FOLDERS,
|
||||
"folder_max_file_count": app.state.config.FOLDER_MAX_FILE_COUNT,
|
||||
"enable_channels": app.state.config.ENABLE_CHANNELS,
|
||||
"enable_notes": app.state.config.ENABLE_NOTES,
|
||||
"enable_web_search": app.state.config.ENABLE_WEB_SEARCH,
|
||||
|
|
@ -1912,10 +1945,12 @@ async def get_app_config(request: Request):
|
|||
"enable_community_sharing": app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||
"enable_message_rating": app.state.config.ENABLE_MESSAGE_RATING,
|
||||
"enable_user_webhooks": app.state.config.ENABLE_USER_WEBHOOKS,
|
||||
"enable_user_status": app.state.config.ENABLE_USER_STATUS,
|
||||
"enable_admin_export": ENABLE_ADMIN_EXPORT,
|
||||
"enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
|
||||
"enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
||||
"enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
|
||||
"enable_memories": app.state.config.ENABLE_MEMORIES,
|
||||
**(
|
||||
{
|
||||
"enable_onedrive_personal": ENABLE_ONEDRIVE_PERSONAL,
|
||||
|
|
@ -2280,8 +2315,13 @@ async def oauth_login(provider: str, request: Request):
|
|||
# - Email addresses are considered unique, so we fail registration if the email address is already taken
|
||||
@app.get("/oauth/{provider}/login/callback")
|
||||
@app.get("/oauth/{provider}/callback") # Legacy endpoint
|
||||
async def oauth_login_callback(provider: str, request: Request, response: Response):
|
||||
return await oauth_manager.handle_callback(request, provider, response)
|
||||
async def oauth_login_callback(
|
||||
provider: str,
|
||||
request: Request,
|
||||
response: Response,
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
return await oauth_manager.handle_callback(request, provider, response, db=db)
|
||||
|
||||
|
||||
@app.get("/manifest.json")
|
||||
|
|
@ -2340,7 +2380,7 @@ async def healthcheck():
|
|||
|
||||
@app.get("/health/db")
|
||||
async def healthcheck_with_db():
|
||||
Session.execute(text("SELECT 1;")).all()
|
||||
ScopedSession.execute(text("SELECT 1;")).all()
|
||||
return {"status": True}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import logging
|
|||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.internal.db import Base, get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
from open_webui.models.users import UserModel, UserProfileImageResponse, Users
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import Boolean, Column, String, Text
|
||||
|
|
@ -87,8 +88,9 @@ class AuthsTable:
|
|||
profile_image_url: str = "/user.png",
|
||||
role: str = "pending",
|
||||
oauth: Optional[dict] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[UserModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
log.info("insert_new_auth")
|
||||
|
||||
id = str(uuid.uuid4())
|
||||
|
|
@ -100,7 +102,7 @@ class AuthsTable:
|
|||
db.add(result)
|
||||
|
||||
user = Users.insert_new_user(
|
||||
id, name, email, profile_image_url, role, oauth=oauth
|
||||
id, name, email, profile_image_url, role, oauth=oauth, db=db
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
|
@ -112,16 +114,16 @@ class AuthsTable:
|
|||
return None
|
||||
|
||||
def authenticate_user(
|
||||
self, email: str, verify_password: callable
|
||||
self, email: str, verify_password: callable, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
log.info(f"authenticate_user: {email}")
|
||||
|
||||
user = Users.get_user_by_email(email)
|
||||
user = Users.get_user_by_email(email, db=db)
|
||||
if not user:
|
||||
return None
|
||||
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
auth = db.query(Auth).filter_by(id=user.id, active=True).first()
|
||||
if auth:
|
||||
if verify_password(auth.password):
|
||||
|
|
@ -133,32 +135,38 @@ class AuthsTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def authenticate_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
|
||||
def authenticate_user_by_api_key(
|
||||
self, api_key: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
log.info(f"authenticate_user_by_api_key: {api_key}")
|
||||
# if no api_key, return None
|
||||
if not api_key:
|
||||
return None
|
||||
|
||||
try:
|
||||
user = Users.get_user_by_api_key(api_key)
|
||||
user = Users.get_user_by_api_key(api_key, db=db)
|
||||
return user if user else None
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def authenticate_user_by_email(self, email: str) -> Optional[UserModel]:
|
||||
def authenticate_user_by_email(
|
||||
self, email: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
log.info(f"authenticate_user_by_email: {email}")
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
auth = db.query(Auth).filter_by(email=email, active=True).first()
|
||||
if auth:
|
||||
user = Users.get_user_by_id(auth.id)
|
||||
user = Users.get_user_by_id(auth.id, db=db)
|
||||
return user
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_user_password_by_id(self, id: str, new_password: str) -> bool:
|
||||
def update_user_password_by_id(
|
||||
self, id: str, new_password: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
result = (
|
||||
db.query(Auth).filter_by(id=id).update({"password": new_password})
|
||||
)
|
||||
|
|
@ -167,20 +175,22 @@ class AuthsTable:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def update_email_by_id(self, id: str, email: str) -> bool:
|
||||
def update_email_by_id(
|
||||
self, id: str, email: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
result = db.query(Auth).filter_by(id=id).update({"email": email})
|
||||
db.commit()
|
||||
return True if result == 1 else False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_auth_by_id(self, id: str) -> bool:
|
||||
def delete_auth_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
# Delete User
|
||||
result = Users.delete_user_by_id(id)
|
||||
result = Users.delete_user_by_id(id, db=db)
|
||||
|
||||
if result:
|
||||
db.query(Auth).filter_by(id=id).delete()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import json
|
||||
import secrets
|
||||
import time
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.internal.db import Base, get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
from open_webui.models.groups import Groups
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
|
@ -244,6 +246,11 @@ class CreateChannelForm(ChannelForm):
|
|||
type: Optional[str] = None
|
||||
|
||||
|
||||
class ChannelWebhookForm(BaseModel):
|
||||
name: str
|
||||
profile_image_url: Optional[str] = None
|
||||
|
||||
|
||||
class ChannelTable:
|
||||
|
||||
def _collect_unique_user_ids(
|
||||
|
|
@ -304,9 +311,9 @@ class ChannelTable:
|
|||
return memberships
|
||||
|
||||
def insert_new_channel(
|
||||
self, form_data: CreateChannelForm, user_id: str
|
||||
self, form_data: CreateChannelForm, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChannelModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
channel = ChannelModel(
|
||||
**{
|
||||
**form_data.model_dump(),
|
||||
|
|
@ -337,8 +344,8 @@ class ChannelTable:
|
|||
db.commit()
|
||||
return channel
|
||||
|
||||
def get_channels(self) -> list[ChannelModel]:
|
||||
with get_db() as db:
|
||||
def get_channels(self, db: Optional[Session] = None) -> list[ChannelModel]:
|
||||
with get_db_context(db) as db:
|
||||
channels = db.query(Channel).all()
|
||||
return [ChannelModel.model_validate(channel) for channel in channels]
|
||||
|
||||
|
|
@ -384,10 +391,12 @@ class ChannelTable:
|
|||
|
||||
return query
|
||||
|
||||
def get_channels_by_user_id(self, user_id: str) -> list[ChannelModel]:
|
||||
with get_db() as db:
|
||||
def get_channels_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> list[ChannelModel]:
|
||||
with get_db_context(db) as db:
|
||||
user_group_ids = [
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id)
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
]
|
||||
|
||||
membership_channels = (
|
||||
|
|
@ -421,8 +430,10 @@ class ChannelTable:
|
|||
all_channels = membership_channels + standard_channels
|
||||
return [ChannelModel.model_validate(c) for c in all_channels]
|
||||
|
||||
def get_dm_channel_by_user_ids(self, user_ids: list[str]) -> Optional[ChannelModel]:
|
||||
with get_db() as db:
|
||||
def get_dm_channel_by_user_ids(
|
||||
self, user_ids: list[str], db: Optional[Session] = None
|
||||
) -> Optional[ChannelModel]:
|
||||
with get_db_context(db) as db:
|
||||
# Ensure uniqueness in case a list with duplicates is passed
|
||||
unique_user_ids = list(set(user_ids))
|
||||
|
||||
|
|
@ -460,8 +471,9 @@ class ChannelTable:
|
|||
invited_by: str,
|
||||
user_ids: Optional[list[str]] = None,
|
||||
group_ids: Optional[list[str]] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> list[ChannelMemberModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
# 1. Collect all user_ids including groups + inviter
|
||||
requested_users = self._collect_unique_user_ids(
|
||||
invited_by, user_ids, group_ids
|
||||
|
|
@ -494,8 +506,9 @@ class ChannelTable:
|
|||
self,
|
||||
channel_id: str,
|
||||
user_ids: list[str],
|
||||
db: Optional[Session] = None,
|
||||
) -> int:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
result = (
|
||||
db.query(ChannelMember)
|
||||
.filter(
|
||||
|
|
@ -507,8 +520,10 @@ class ChannelTable:
|
|||
db.commit()
|
||||
return result # number of rows deleted
|
||||
|
||||
def is_user_channel_manager(self, channel_id: str, user_id: str) -> bool:
|
||||
with get_db() as db:
|
||||
def is_user_channel_manager(
|
||||
self, channel_id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
# Check if the user is the creator of the channel
|
||||
# or has a 'manager' role in ChannelMember
|
||||
channel = db.query(Channel).filter(Channel.id == channel_id).first()
|
||||
|
|
@ -527,9 +542,9 @@ class ChannelTable:
|
|||
return membership is not None
|
||||
|
||||
def join_channel(
|
||||
self, channel_id: str, user_id: str
|
||||
self, channel_id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChannelMemberModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
# Check if the membership already exists
|
||||
existing_membership = (
|
||||
db.query(ChannelMember)
|
||||
|
|
@ -565,8 +580,10 @@ class ChannelTable:
|
|||
db.commit()
|
||||
return channel_member
|
||||
|
||||
def leave_channel(self, channel_id: str, user_id: str) -> bool:
|
||||
with get_db() as db:
|
||||
def leave_channel(
|
||||
self, channel_id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
membership = (
|
||||
db.query(ChannelMember)
|
||||
.filter(
|
||||
|
|
@ -587,9 +604,9 @@ class ChannelTable:
|
|||
return True
|
||||
|
||||
def get_member_by_channel_and_user_id(
|
||||
self, channel_id: str, user_id: str
|
||||
self, channel_id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChannelMemberModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
membership = (
|
||||
db.query(ChannelMember)
|
||||
.filter(
|
||||
|
|
@ -600,8 +617,10 @@ class ChannelTable:
|
|||
)
|
||||
return ChannelMemberModel.model_validate(membership) if membership else None
|
||||
|
||||
def get_members_by_channel_id(self, channel_id: str) -> list[ChannelMemberModel]:
|
||||
with get_db() as db:
|
||||
def get_members_by_channel_id(
|
||||
self, channel_id: str, db: Optional[Session] = None
|
||||
) -> list[ChannelMemberModel]:
|
||||
with get_db_context(db) as db:
|
||||
memberships = (
|
||||
db.query(ChannelMember)
|
||||
.filter(ChannelMember.channel_id == channel_id)
|
||||
|
|
@ -612,8 +631,14 @@ class ChannelTable:
|
|||
for membership in memberships
|
||||
]
|
||||
|
||||
def pin_channel(self, channel_id: str, user_id: str, is_pinned: bool) -> bool:
|
||||
with get_db() as db:
|
||||
def pin_channel(
|
||||
self,
|
||||
channel_id: str,
|
||||
user_id: str,
|
||||
is_pinned: bool,
|
||||
db: Optional[Session] = None,
|
||||
) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
membership = (
|
||||
db.query(ChannelMember)
|
||||
.filter(
|
||||
|
|
@ -631,8 +656,10 @@ class ChannelTable:
|
|||
db.commit()
|
||||
return True
|
||||
|
||||
def update_member_last_read_at(self, channel_id: str, user_id: str) -> bool:
|
||||
with get_db() as db:
|
||||
def update_member_last_read_at(
|
||||
self, channel_id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
membership = (
|
||||
db.query(ChannelMember)
|
||||
.filter(
|
||||
|
|
@ -651,9 +678,13 @@ class ChannelTable:
|
|||
return True
|
||||
|
||||
def update_member_active_status(
|
||||
self, channel_id: str, user_id: str, is_active: bool
|
||||
self,
|
||||
channel_id: str,
|
||||
user_id: str,
|
||||
is_active: bool,
|
||||
db: Optional[Session] = None,
|
||||
) -> bool:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
membership = (
|
||||
db.query(ChannelMember)
|
||||
.filter(
|
||||
|
|
@ -671,8 +702,10 @@ class ChannelTable:
|
|||
db.commit()
|
||||
return True
|
||||
|
||||
def is_user_channel_member(self, channel_id: str, user_id: str) -> bool:
|
||||
with get_db() as db:
|
||||
def is_user_channel_member(
|
||||
self, channel_id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
membership = (
|
||||
db.query(ChannelMember)
|
||||
.filter(
|
||||
|
|
@ -683,13 +716,20 @@ class ChannelTable:
|
|||
)
|
||||
return membership is not None
|
||||
|
||||
def get_channel_by_id(self, id: str) -> Optional[ChannelModel]:
|
||||
with get_db() as db:
|
||||
channel = db.query(Channel).filter(Channel.id == id).first()
|
||||
return ChannelModel.model_validate(channel) if channel else None
|
||||
def get_channel_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChannelModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
channel = db.query(Channel).filter(Channel.id == id).first()
|
||||
return ChannelModel.model_validate(channel) if channel else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_channels_by_file_id(self, file_id: str) -> list[ChannelModel]:
|
||||
with get_db() as db:
|
||||
def get_channels_by_file_id(
|
||||
self, file_id: str, db: Optional[Session] = None
|
||||
) -> list[ChannelModel]:
|
||||
with get_db_context(db) as db:
|
||||
channel_files = (
|
||||
db.query(ChannelFile).filter(ChannelFile.file_id == file_id).all()
|
||||
)
|
||||
|
|
@ -698,9 +738,9 @@ class ChannelTable:
|
|||
return [ChannelModel.model_validate(channel) for channel in channels]
|
||||
|
||||
def get_channels_by_file_id_and_user_id(
|
||||
self, file_id: str, user_id: str
|
||||
self, file_id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> list[ChannelModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
# 1. Determine which channels have this file
|
||||
channel_file_rows = (
|
||||
db.query(ChannelFile).filter(ChannelFile.file_id == file_id).all()
|
||||
|
|
@ -724,7 +764,9 @@ class ChannelTable:
|
|||
return []
|
||||
|
||||
# Preload user's group membership
|
||||
user_group_ids = [g.id for g in Groups.get_groups_by_member_id(user_id)]
|
||||
user_group_ids = [
|
||||
g.id for g in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
]
|
||||
|
||||
allowed_channels = []
|
||||
|
||||
|
|
@ -761,9 +803,9 @@ class ChannelTable:
|
|||
return allowed_channels
|
||||
|
||||
def get_channel_by_id_and_user_id(
|
||||
self, id: str, user_id: str
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChannelModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
# Fetch the channel
|
||||
channel: Channel = (
|
||||
db.query(Channel)
|
||||
|
|
@ -799,7 +841,7 @@ class ChannelTable:
|
|||
|
||||
# Determine user groups
|
||||
user_group_ids = [
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id)
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
]
|
||||
|
||||
# Apply ACL rules
|
||||
|
|
@ -818,9 +860,9 @@ class ChannelTable:
|
|||
)
|
||||
|
||||
def update_channel_by_id(
|
||||
self, id: str, form_data: ChannelForm
|
||||
self, id: str, form_data: ChannelForm, db: Optional[Session] = None
|
||||
) -> Optional[ChannelModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
channel = db.query(Channel).filter(Channel.id == id).first()
|
||||
if not channel:
|
||||
return None
|
||||
|
|
@ -839,9 +881,9 @@ class ChannelTable:
|
|||
return ChannelModel.model_validate(channel) if channel else None
|
||||
|
||||
def add_file_to_channel_by_id(
|
||||
self, channel_id: str, file_id: str, user_id: str
|
||||
self, channel_id: str, file_id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChannelFileModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
channel_file = ChannelFileModel(
|
||||
**{
|
||||
"id": str(uuid.uuid4()),
|
||||
|
|
@ -866,10 +908,14 @@ class ChannelTable:
|
|||
return None
|
||||
|
||||
def set_file_message_id_in_channel_by_id(
|
||||
self, channel_id: str, file_id: str, message_id: str
|
||||
self,
|
||||
channel_id: str,
|
||||
file_id: str,
|
||||
message_id: str,
|
||||
db: Optional[Session] = None,
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
channel_file = (
|
||||
db.query(ChannelFile)
|
||||
.filter_by(channel_id=channel_id, file_id=file_id)
|
||||
|
|
@ -886,9 +932,11 @@ class ChannelTable:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def remove_file_from_channel_by_id(self, channel_id: str, file_id: str) -> bool:
|
||||
def remove_file_from_channel_by_id(
|
||||
self, channel_id: str, file_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(ChannelFile).filter_by(
|
||||
channel_id=channel_id, file_id=file_id
|
||||
).delete()
|
||||
|
|
@ -897,11 +945,115 @@ class ChannelTable:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_channel_by_id(self, id: str):
|
||||
with get_db() as db:
|
||||
def delete_channel_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Channel).filter(Channel.id == id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
####################
|
||||
# Webhook Methods
|
||||
####################
|
||||
|
||||
def insert_webhook(
|
||||
self,
|
||||
channel_id: str,
|
||||
user_id: str,
|
||||
form_data: ChannelWebhookForm,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[ChannelWebhookModel]:
|
||||
with get_db_context(db) as db:
|
||||
webhook = ChannelWebhookModel(
|
||||
id=str(uuid.uuid4()),
|
||||
channel_id=channel_id,
|
||||
user_id=user_id,
|
||||
name=form_data.name,
|
||||
profile_image_url=form_data.profile_image_url,
|
||||
token=secrets.token_urlsafe(32),
|
||||
last_used_at=None,
|
||||
created_at=int(time.time_ns()),
|
||||
updated_at=int(time.time_ns()),
|
||||
)
|
||||
db.add(ChannelWebhook(**webhook.model_dump()))
|
||||
db.commit()
|
||||
return webhook
|
||||
|
||||
def get_webhooks_by_channel_id(
|
||||
self, channel_id: str, db: Optional[Session] = None
|
||||
) -> list[ChannelWebhookModel]:
|
||||
with get_db_context(db) as db:
|
||||
webhooks = (
|
||||
db.query(ChannelWebhook)
|
||||
.filter(ChannelWebhook.channel_id == channel_id)
|
||||
.all()
|
||||
)
|
||||
return [ChannelWebhookModel.model_validate(w) for w in webhooks]
|
||||
|
||||
def get_webhook_by_id(
|
||||
self, webhook_id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChannelWebhookModel]:
|
||||
with get_db_context(db) as db:
|
||||
webhook = (
|
||||
db.query(ChannelWebhook).filter(ChannelWebhook.id == webhook_id).first()
|
||||
)
|
||||
return ChannelWebhookModel.model_validate(webhook) if webhook else None
|
||||
|
||||
def get_webhook_by_id_and_token(
|
||||
self, webhook_id: str, token: str, db: Optional[Session] = None
|
||||
) -> Optional[ChannelWebhookModel]:
|
||||
with get_db_context(db) as db:
|
||||
webhook = (
|
||||
db.query(ChannelWebhook)
|
||||
.filter(
|
||||
ChannelWebhook.id == webhook_id,
|
||||
ChannelWebhook.token == token,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
return ChannelWebhookModel.model_validate(webhook) if webhook else None
|
||||
|
||||
def update_webhook_by_id(
|
||||
self,
|
||||
webhook_id: str,
|
||||
form_data: ChannelWebhookForm,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[ChannelWebhookModel]:
|
||||
with get_db_context(db) as db:
|
||||
webhook = (
|
||||
db.query(ChannelWebhook).filter(ChannelWebhook.id == webhook_id).first()
|
||||
)
|
||||
if not webhook:
|
||||
return None
|
||||
webhook.name = form_data.name
|
||||
webhook.profile_image_url = form_data.profile_image_url
|
||||
webhook.updated_at = int(time.time_ns())
|
||||
db.commit()
|
||||
return ChannelWebhookModel.model_validate(webhook)
|
||||
|
||||
def update_webhook_last_used_at(
|
||||
self, webhook_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
webhook = (
|
||||
db.query(ChannelWebhook).filter(ChannelWebhook.id == webhook_id).first()
|
||||
)
|
||||
if not webhook:
|
||||
return False
|
||||
webhook.last_used_at = int(time.time_ns())
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
def delete_webhook_by_id(
|
||||
self, webhook_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
result = (
|
||||
db.query(ChannelWebhook)
|
||||
.filter(ChannelWebhook.id == webhook_id)
|
||||
.delete()
|
||||
)
|
||||
db.commit()
|
||||
return result > 0
|
||||
|
||||
|
||||
Channels = ChannelTable()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import time
|
|||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.internal.db import Base, get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
from open_webui.models.tags import TagModel, Tag, Tags
|
||||
from open_webui.models.folders import Folders
|
||||
from open_webui.utils.misc import sanitize_data_for_db, sanitize_text_for_db
|
||||
|
|
@ -210,6 +211,48 @@ class ChatUsageStatsListResponse(BaseModel):
|
|||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
class MessageStats(BaseModel):
|
||||
id: str
|
||||
role: str
|
||||
model: Optional[str] = None
|
||||
content_length: int
|
||||
token_count: Optional[int] = None
|
||||
timestamp: Optional[int] = None
|
||||
rating: Optional[int] = None # Derived from message.annotation.rating
|
||||
tags: Optional[list[str]] = None # Derived from message.annotation.tags
|
||||
|
||||
|
||||
class ChatHistoryStats(BaseModel):
|
||||
messages: dict[str, MessageStats]
|
||||
currentId: Optional[str] = None
|
||||
|
||||
|
||||
class ChatBody(BaseModel):
|
||||
history: ChatHistoryStats
|
||||
|
||||
|
||||
class AggregateChatStats(BaseModel):
|
||||
average_response_time: float
|
||||
average_user_message_content_length: float
|
||||
average_assistant_message_content_length: float
|
||||
models: dict[str, int]
|
||||
message_count: int
|
||||
history_models: dict[str, int]
|
||||
history_message_count: int
|
||||
history_user_message_count: int
|
||||
history_assistant_message_count: int
|
||||
|
||||
|
||||
class ChatStatsExport(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
created_at: int
|
||||
updated_at: int
|
||||
tags: list[str] = []
|
||||
stats: AggregateChatStats
|
||||
chat: ChatBody
|
||||
|
||||
|
||||
class ChatTable:
|
||||
def _clean_null_bytes(self, obj):
|
||||
"""Recursively remove null bytes from strings in dict/list structures."""
|
||||
|
|
@ -238,8 +281,10 @@ class ChatTable:
|
|||
|
||||
return changed
|
||||
|
||||
def insert_new_chat(self, user_id: str, form_data: ChatForm) -> Optional[ChatModel]:
|
||||
with get_db() as db:
|
||||
def insert_new_chat(
|
||||
self, user_id: str, form_data: ChatForm, db: Optional[Session] = None
|
||||
) -> Optional[ChatModel]:
|
||||
with get_db_context(db) as db:
|
||||
id = str(uuid.uuid4())
|
||||
chat = ChatModel(
|
||||
**{
|
||||
|
|
@ -289,9 +334,12 @@ class ChatTable:
|
|||
return chat
|
||||
|
||||
def import_chats(
|
||||
self, user_id: str, chat_import_forms: list[ChatImportForm]
|
||||
self,
|
||||
user_id: str,
|
||||
chat_import_forms: list[ChatImportForm],
|
||||
db: Optional[Session] = None,
|
||||
) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
chats = []
|
||||
|
||||
for form_data in chat_import_forms:
|
||||
|
|
@ -302,9 +350,11 @@ class ChatTable:
|
|||
db.commit()
|
||||
return [ChatModel.model_validate(chat) for chat in chats]
|
||||
|
||||
def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
|
||||
def update_chat_by_id(
|
||||
self, id: str, chat: dict, db: Optional[Session] = None
|
||||
) -> Optional[ChatModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
chat_item = db.get(Chat, id)
|
||||
chat_item.chat = self._clean_null_bytes(chat)
|
||||
chat_item.title = (
|
||||
|
|
@ -423,31 +473,37 @@ class ChatTable:
|
|||
def add_message_files_by_id_and_message_id(
|
||||
self, id: str, message_id: str, files: list[dict]
|
||||
) -> list[dict]:
|
||||
chat = self.get_chat_by_id(id)
|
||||
if chat is None:
|
||||
return None
|
||||
with get_db_context() as db:
|
||||
chat = self.get_chat_by_id(id, db=db)
|
||||
if chat is None:
|
||||
return None
|
||||
|
||||
chat = chat.chat
|
||||
history = chat.get("history", {})
|
||||
chat = chat.chat
|
||||
history = chat.get("history", {})
|
||||
|
||||
message_files = []
|
||||
message_files = []
|
||||
|
||||
if message_id in history.get("messages", {}):
|
||||
message_files = history["messages"][message_id].get("files", [])
|
||||
message_files = message_files + files
|
||||
history["messages"][message_id]["files"] = message_files
|
||||
if message_id in history.get("messages", {}):
|
||||
message_files = history["messages"][message_id].get("files", [])
|
||||
message_files = message_files + files
|
||||
history["messages"][message_id]["files"] = message_files
|
||||
|
||||
chat["history"] = history
|
||||
self.update_chat_by_id(id, chat)
|
||||
return message_files
|
||||
chat["history"] = history
|
||||
self.update_chat_by_id(id, chat, db=db)
|
||||
return message_files
|
||||
|
||||
def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
|
||||
with get_db() as db:
|
||||
def insert_shared_chat_by_chat_id(
|
||||
self, chat_id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChatModel]:
|
||||
with get_db_context(db) as db:
|
||||
# Get the existing chat to share
|
||||
chat = db.get(Chat, chat_id)
|
||||
# Check if chat exists
|
||||
if not chat:
|
||||
return None
|
||||
# Check if the chat is already shared
|
||||
if chat.share_id:
|
||||
return self.get_chat_by_id_and_user_id(chat.share_id, "shared")
|
||||
return self.get_chat_by_id_and_user_id(chat.share_id, "shared", db=db)
|
||||
# Create a new chat with the same data, but with a new ID
|
||||
shared_chat = ChatModel(
|
||||
**{
|
||||
|
|
@ -476,16 +532,18 @@ class ChatTable:
|
|||
db.commit()
|
||||
return shared_chat if (shared_result and result) else None
|
||||
|
||||
def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
|
||||
def update_shared_chat_by_chat_id(
|
||||
self, chat_id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChatModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
chat = db.get(Chat, chat_id)
|
||||
shared_chat = (
|
||||
db.query(Chat).filter_by(user_id=f"shared-{chat_id}").first()
|
||||
)
|
||||
|
||||
if shared_chat is None:
|
||||
return self.insert_shared_chat_by_chat_id(chat_id)
|
||||
return self.insert_shared_chat_by_chat_id(chat_id, db=db)
|
||||
|
||||
shared_chat.title = chat.title
|
||||
shared_chat.chat = chat.chat
|
||||
|
|
@ -500,9 +558,11 @@ class ChatTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool:
|
||||
def delete_shared_chat_by_chat_id(
|
||||
self, chat_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Chat).filter_by(user_id=f"shared-{chat_id}").delete()
|
||||
db.commit()
|
||||
|
||||
|
|
@ -510,9 +570,11 @@ class ChatTable:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def unarchive_all_chats_by_user_id(self, user_id: str) -> bool:
|
||||
def unarchive_all_chats_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Chat).filter_by(user_id=user_id).update({"archived": False})
|
||||
db.commit()
|
||||
return True
|
||||
|
|
@ -520,10 +582,10 @@ class ChatTable:
|
|||
return False
|
||||
|
||||
def update_chat_share_id_by_id(
|
||||
self, id: str, share_id: Optional[str]
|
||||
self, id: str, share_id: Optional[str], db: Optional[Session] = None
|
||||
) -> Optional[ChatModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
chat = db.get(Chat, id)
|
||||
chat.share_id = share_id
|
||||
db.commit()
|
||||
|
|
@ -532,9 +594,11 @@ class ChatTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def toggle_chat_pinned_by_id(self, id: str) -> Optional[ChatModel]:
|
||||
def toggle_chat_pinned_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChatModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
chat = db.get(Chat, id)
|
||||
chat.pinned = not chat.pinned
|
||||
chat.updated_at = int(time.time())
|
||||
|
|
@ -544,9 +608,11 @@ class ChatTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]:
|
||||
def toggle_chat_archive_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChatModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
chat = db.get(Chat, id)
|
||||
chat.archived = not chat.archived
|
||||
chat.folder_id = None
|
||||
|
|
@ -557,9 +623,11 @@ class ChatTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def archive_all_chats_by_user_id(self, user_id: str) -> bool:
|
||||
def archive_all_chats_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Chat).filter_by(user_id=user_id).update({"archived": True})
|
||||
db.commit()
|
||||
return True
|
||||
|
|
@ -572,9 +640,10 @@ class ChatTable:
|
|||
filter: Optional[dict] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: Optional[Session] = None,
|
||||
) -> list[ChatModel]:
|
||||
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Chat).filter_by(user_id=user_id, archived=True)
|
||||
|
||||
if filter:
|
||||
|
|
@ -613,8 +682,9 @@ class ChatTable:
|
|||
filter: Optional[dict] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: Optional[Session] = None,
|
||||
) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Chat).filter_by(user_id=user_id)
|
||||
if not include_archived:
|
||||
query = query.filter_by(archived=False)
|
||||
|
|
@ -653,8 +723,9 @@ class ChatTable:
|
|||
include_pinned: bool = False,
|
||||
skip: Optional[int] = None,
|
||||
limit: Optional[int] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> list[ChatTitleIdResponse]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Chat).filter_by(user_id=user_id)
|
||||
|
||||
if not include_folders:
|
||||
|
|
@ -691,9 +762,13 @@ class ChatTable:
|
|||
]
|
||||
|
||||
def get_chat_list_by_chat_ids(
|
||||
self, chat_ids: list[str], skip: int = 0, limit: int = 50
|
||||
self,
|
||||
chat_ids: list[str],
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: Optional[Session] = None,
|
||||
) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
all_chats = (
|
||||
db.query(Chat)
|
||||
.filter(Chat.id.in_(chat_ids))
|
||||
|
|
@ -703,9 +778,11 @@ class ChatTable:
|
|||
)
|
||||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||
|
||||
def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
|
||||
def get_chat_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChatModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
chat_item = db.get(Chat, id)
|
||||
if chat_item is None:
|
||||
return None
|
||||
|
|
@ -718,30 +795,36 @@ class ChatTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]:
|
||||
def get_chat_by_share_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChatModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
# it is possible that the shared link was deleted. hence,
|
||||
# we check if the chat is still shared by checking if a chat with the share_id exists
|
||||
chat = db.query(Chat).filter_by(share_id=id).first()
|
||||
|
||||
if chat:
|
||||
return self.get_chat_by_id(id)
|
||||
return self.get_chat_by_id(id, db=db)
|
||||
else:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
|
||||
def get_chat_by_id_and_user_id(
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChatModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
chat = db.query(Chat).filter_by(id=id, user_id=user_id).first()
|
||||
return ChatModel.model_validate(chat)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_chats(self, skip: int = 0, limit: int = 50) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
def get_chats(
|
||||
self, skip: int = 0, limit: int = 50, db: Optional[Session] = None
|
||||
) -> list[ChatModel]:
|
||||
with get_db_context(db) as db:
|
||||
all_chats = (
|
||||
db.query(Chat)
|
||||
# .limit(limit).offset(skip)
|
||||
|
|
@ -750,14 +833,34 @@ class ChatTable:
|
|||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||
|
||||
def get_chats_by_user_id(
|
||||
self, user_id: str, skip: Optional[int] = None, limit: Optional[int] = None
|
||||
self,
|
||||
user_id: str,
|
||||
filter: Optional[dict] = None,
|
||||
skip: Optional[int] = None,
|
||||
limit: Optional[int] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> ChatListResponse:
|
||||
with get_db() as db:
|
||||
query = (
|
||||
db.query(Chat)
|
||||
.filter_by(user_id=user_id)
|
||||
.order_by(Chat.updated_at.desc())
|
||||
)
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Chat).filter_by(user_id=user_id)
|
||||
|
||||
if filter:
|
||||
if filter.get("updated_at"):
|
||||
query = query.filter(Chat.updated_at > filter.get("updated_at"))
|
||||
|
||||
order_by = filter.get("order_by")
|
||||
direction = filter.get("direction")
|
||||
|
||||
if order_by and direction:
|
||||
if hasattr(Chat, order_by):
|
||||
if direction.lower() == "asc":
|
||||
query = query.order_by(getattr(Chat, order_by).asc())
|
||||
elif direction.lower() == "desc":
|
||||
query = query.order_by(getattr(Chat, order_by).desc())
|
||||
else:
|
||||
query = query.order_by(Chat.updated_at.desc())
|
||||
|
||||
else:
|
||||
query = query.order_by(Chat.updated_at.desc())
|
||||
|
||||
total = query.count()
|
||||
|
||||
|
|
@ -775,8 +878,10 @@ class ChatTable:
|
|||
}
|
||||
)
|
||||
|
||||
def get_pinned_chats_by_user_id(self, user_id: str) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
def get_pinned_chats_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> list[ChatModel]:
|
||||
with get_db_context(db) as db:
|
||||
all_chats = (
|
||||
db.query(Chat)
|
||||
.filter_by(user_id=user_id, pinned=True, archived=False)
|
||||
|
|
@ -784,8 +889,10 @@ class ChatTable:
|
|||
)
|
||||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||
|
||||
def get_archived_chats_by_user_id(self, user_id: str) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
def get_archived_chats_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> list[ChatModel]:
|
||||
with get_db_context(db) as db:
|
||||
all_chats = (
|
||||
db.query(Chat)
|
||||
.filter_by(user_id=user_id, archived=True)
|
||||
|
|
@ -800,6 +907,7 @@ class ChatTable:
|
|||
include_archived: bool = False,
|
||||
skip: int = 0,
|
||||
limit: int = 60,
|
||||
db: Optional[Session] = None,
|
||||
) -> list[ChatModel]:
|
||||
"""
|
||||
Filters chats based on a search query using Python, allowing pagination using skip and limit.
|
||||
|
|
@ -808,7 +916,7 @@ class ChatTable:
|
|||
|
||||
if not search_text:
|
||||
return self.get_chat_list_by_user_id(
|
||||
user_id, include_archived, filter={}, skip=skip, limit=limit
|
||||
user_id, include_archived, filter={}, skip=skip, limit=limit, db=db
|
||||
)
|
||||
|
||||
search_text_words = search_text.split(" ")
|
||||
|
|
@ -863,7 +971,7 @@ class ChatTable:
|
|||
|
||||
search_text = " ".join(search_text_words)
|
||||
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Chat).filter(Chat.user_id == user_id)
|
||||
|
||||
if is_archived is not None:
|
||||
|
|
@ -1004,9 +1112,14 @@ class ChatTable:
|
|||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||
|
||||
def get_chats_by_folder_id_and_user_id(
|
||||
self, folder_id: str, user_id: str, skip: int = 0, limit: int = 60
|
||||
self,
|
||||
folder_id: str,
|
||||
user_id: str,
|
||||
skip: int = 0,
|
||||
limit: int = 60,
|
||||
db: Optional[Session] = None,
|
||||
) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Chat).filter_by(folder_id=folder_id, user_id=user_id)
|
||||
query = query.filter(or_(Chat.pinned == False, Chat.pinned == None))
|
||||
query = query.filter_by(archived=False)
|
||||
|
|
@ -1022,9 +1135,9 @@ class ChatTable:
|
|||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||
|
||||
def get_chats_by_folder_ids_and_user_id(
|
||||
self, folder_ids: list[str], user_id: str
|
||||
self, folder_ids: list[str], user_id: str, db: Optional[Session] = None
|
||||
) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Chat).filter(
|
||||
Chat.folder_id.in_(folder_ids), Chat.user_id == user_id
|
||||
)
|
||||
|
|
@ -1037,10 +1150,10 @@ class ChatTable:
|
|||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||
|
||||
def update_chat_folder_id_by_id_and_user_id(
|
||||
self, id: str, user_id: str, folder_id: str
|
||||
self, id: str, user_id: str, folder_id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChatModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
chat = db.get(Chat, id)
|
||||
chat.folder_id = folder_id
|
||||
chat.updated_at = int(time.time())
|
||||
|
|
@ -1051,16 +1164,23 @@ class ChatTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def get_chat_tags_by_id_and_user_id(self, id: str, user_id: str) -> list[TagModel]:
|
||||
with get_db() as db:
|
||||
def get_chat_tags_by_id_and_user_id(
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> list[TagModel]:
|
||||
with get_db_context(db) as db:
|
||||
chat = db.get(Chat, id)
|
||||
tags = chat.meta.get("tags", [])
|
||||
return [Tags.get_tag_by_name_and_user_id(tag, user_id) for tag in tags]
|
||||
|
||||
def get_chat_list_by_user_id_and_tag_name(
|
||||
self, user_id: str, tag_name: str, skip: int = 0, limit: int = 50
|
||||
self,
|
||||
user_id: str,
|
||||
tag_name: str,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: Optional[Session] = None,
|
||||
) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Chat).filter_by(user_id=user_id)
|
||||
tag_id = tag_name.replace(" ", "_").lower()
|
||||
|
||||
|
|
@ -1089,13 +1209,13 @@ class ChatTable:
|
|||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||
|
||||
def add_chat_tag_by_id_and_user_id_and_tag_name(
|
||||
self, id: str, user_id: str, tag_name: str
|
||||
self, id: str, user_id: str, tag_name: str, db: Optional[Session] = None
|
||||
) -> Optional[ChatModel]:
|
||||
tag = Tags.get_tag_by_name_and_user_id(tag_name, user_id)
|
||||
if tag is None:
|
||||
tag = Tags.insert_new_tag(tag_name, user_id)
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
chat = db.get(Chat, id)
|
||||
|
||||
tag_id = tag.id
|
||||
|
|
@ -1111,8 +1231,10 @@ class ChatTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def count_chats_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> int:
|
||||
with get_db() as db: # Assuming `get_db()` returns a session object
|
||||
def count_chats_by_tag_name_and_user_id(
|
||||
self, tag_name: str, user_id: str, db: Optional[Session] = None
|
||||
) -> int:
|
||||
with get_db_context(db) as db: # Assuming `get_db()` returns a session object
|
||||
query = db.query(Chat).filter_by(user_id=user_id, archived=False)
|
||||
|
||||
# Normalize the tag_name for consistency
|
||||
|
|
@ -1147,8 +1269,10 @@ class ChatTable:
|
|||
|
||||
return count
|
||||
|
||||
def count_chats_by_folder_id_and_user_id(self, folder_id: str, user_id: str) -> int:
|
||||
with get_db() as db:
|
||||
def count_chats_by_folder_id_and_user_id(
|
||||
self, folder_id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> int:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Chat).filter_by(user_id=user_id)
|
||||
|
||||
query = query.filter_by(folder_id=folder_id)
|
||||
|
|
@ -1158,10 +1282,10 @@ class ChatTable:
|
|||
return count
|
||||
|
||||
def delete_tag_by_id_and_user_id_and_tag_name(
|
||||
self, id: str, user_id: str, tag_name: str
|
||||
self, id: str, user_id: str, tag_name: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
chat = db.get(Chat, id)
|
||||
tags = chat.meta.get("tags", [])
|
||||
tag_id = tag_name.replace(" ", "_").lower()
|
||||
|
|
@ -1176,9 +1300,11 @@ class ChatTable:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_all_tags_by_id_and_user_id(self, id: str, user_id: str) -> bool:
|
||||
def delete_all_tags_by_id_and_user_id(
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
chat = db.get(Chat, id)
|
||||
chat.meta = {
|
||||
**chat.meta,
|
||||
|
|
@ -1190,30 +1316,34 @@ class ChatTable:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_chat_by_id(self, id: str) -> bool:
|
||||
def delete_chat_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Chat).filter_by(id=id).delete()
|
||||
db.commit()
|
||||
|
||||
return True and self.delete_shared_chat_by_chat_id(id)
|
||||
return True and self.delete_shared_chat_by_chat_id(id, db=db)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool:
|
||||
def delete_chat_by_id_and_user_id(
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Chat).filter_by(id=id, user_id=user_id).delete()
|
||||
db.commit()
|
||||
|
||||
return True and self.delete_shared_chat_by_chat_id(id)
|
||||
return True and self.delete_shared_chat_by_chat_id(id, db=db)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_chats_by_user_id(self, user_id: str) -> bool:
|
||||
def delete_chats_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
self.delete_shared_chats_by_user_id(user_id)
|
||||
with get_db_context(db) as db:
|
||||
self.delete_shared_chats_by_user_id(user_id, db=db)
|
||||
|
||||
db.query(Chat).filter_by(user_id=user_id).delete()
|
||||
db.commit()
|
||||
|
|
@ -1223,10 +1353,10 @@ class ChatTable:
|
|||
return False
|
||||
|
||||
def delete_chats_by_user_id_and_folder_id(
|
||||
self, user_id: str, folder_id: str
|
||||
self, user_id: str, folder_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Chat).filter_by(user_id=user_id, folder_id=folder_id).delete()
|
||||
db.commit()
|
||||
|
||||
|
|
@ -1235,10 +1365,14 @@ class ChatTable:
|
|||
return False
|
||||
|
||||
def move_chats_by_user_id_and_folder_id(
|
||||
self, user_id: str, folder_id: str, new_folder_id: Optional[str]
|
||||
self,
|
||||
user_id: str,
|
||||
folder_id: str,
|
||||
new_folder_id: Optional[str],
|
||||
db: Optional[Session] = None,
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Chat).filter_by(user_id=user_id, folder_id=folder_id).update(
|
||||
{"folder_id": new_folder_id}
|
||||
)
|
||||
|
|
@ -1248,9 +1382,11 @@ class ChatTable:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
|
||||
def delete_shared_chats_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
chats_by_user = db.query(Chat).filter_by(user_id=user_id).all()
|
||||
shared_chat_ids = [f"shared-{chat.id}" for chat in chats_by_user]
|
||||
|
||||
|
|
@ -1262,7 +1398,12 @@ class ChatTable:
|
|||
return False
|
||||
|
||||
def insert_chat_files(
|
||||
self, chat_id: str, message_id: str, file_ids: list[str], user_id: str
|
||||
self,
|
||||
chat_id: str,
|
||||
message_id: str,
|
||||
file_ids: list[str],
|
||||
user_id: str,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[list[ChatFileModel]]:
|
||||
if not file_ids:
|
||||
return None
|
||||
|
|
@ -1270,7 +1411,7 @@ class ChatTable:
|
|||
chat_message_file_ids = [
|
||||
item.id
|
||||
for item in self.get_chat_files_by_chat_id_and_message_id(
|
||||
chat_id, message_id
|
||||
chat_id, message_id, db=db
|
||||
)
|
||||
]
|
||||
# Remove duplicates and existing file_ids
|
||||
|
|
@ -1287,7 +1428,7 @@ class ChatTable:
|
|||
return None
|
||||
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
now = int(time.time())
|
||||
|
||||
chat_files = [
|
||||
|
|
@ -1315,9 +1456,9 @@ class ChatTable:
|
|||
return None
|
||||
|
||||
def get_chat_files_by_chat_id_and_message_id(
|
||||
self, chat_id: str, message_id: str
|
||||
self, chat_id: str, message_id: str, db: Optional[Session] = None
|
||||
) -> list[ChatFileModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
all_chat_files = (
|
||||
db.query(ChatFile)
|
||||
.filter_by(chat_id=chat_id, message_id=message_id)
|
||||
|
|
@ -1328,17 +1469,21 @@ class ChatTable:
|
|||
ChatFileModel.model_validate(chat_file) for chat_file in all_chat_files
|
||||
]
|
||||
|
||||
def delete_chat_file(self, chat_id: str, file_id: str) -> bool:
|
||||
def delete_chat_file(
|
||||
self, chat_id: str, file_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(ChatFile).filter_by(chat_id=chat_id, file_id=file_id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_shared_chats_by_file_id(self, file_id: str) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
def get_shared_chats_by_file_id(
|
||||
self, file_id: str, db: Optional[Session] = None
|
||||
) -> list[ChatModel]:
|
||||
with get_db_context(db) as db:
|
||||
# Join Chat and ChatFile tables to get shared chats associated with the file_id
|
||||
all_chats = (
|
||||
db.query(Chat)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import time
|
|||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.internal.db import Base, get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
from open_webui.models.users import User
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
|
@ -67,6 +68,13 @@ class FeedbackIdResponse(BaseModel):
|
|||
updated_at: int
|
||||
|
||||
|
||||
class LeaderboardFeedbackData(BaseModel):
|
||||
"""Minimal feedback data for leaderboard computation (excludes snapshot/meta)."""
|
||||
|
||||
id: str
|
||||
data: Optional[dict] = None
|
||||
|
||||
|
||||
class RatingData(BaseModel):
|
||||
rating: Optional[str | int] = None
|
||||
model_id: Optional[str] = None
|
||||
|
|
@ -119,11 +127,22 @@ class FeedbackListResponse(BaseModel):
|
|||
total: int
|
||||
|
||||
|
||||
class ModelHistoryEntry(BaseModel):
|
||||
date: str
|
||||
won: int
|
||||
lost: int
|
||||
|
||||
|
||||
class ModelHistoryResponse(BaseModel):
|
||||
model_id: str
|
||||
history: list[ModelHistoryEntry]
|
||||
|
||||
|
||||
class FeedbackTable:
|
||||
def insert_new_feedback(
|
||||
self, user_id: str, form_data: FeedbackForm
|
||||
self, user_id: str, form_data: FeedbackForm, db: Optional[Session] = None
|
||||
) -> Optional[FeedbackModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
id = str(uuid.uuid4())
|
||||
feedback = FeedbackModel(
|
||||
**{
|
||||
|
|
@ -148,9 +167,11 @@ class FeedbackTable:
|
|||
log.exception(f"Error creating a new feedback: {e}")
|
||||
return None
|
||||
|
||||
def get_feedback_by_id(self, id: str) -> Optional[FeedbackModel]:
|
||||
def get_feedback_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[FeedbackModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
feedback = db.query(Feedback).filter_by(id=id).first()
|
||||
if not feedback:
|
||||
return None
|
||||
|
|
@ -159,10 +180,10 @@ class FeedbackTable:
|
|||
return None
|
||||
|
||||
def get_feedback_by_id_and_user_id(
|
||||
self, id: str, user_id: str
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[FeedbackModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
feedback = db.query(Feedback).filter_by(id=id, user_id=user_id).first()
|
||||
if not feedback:
|
||||
return None
|
||||
|
|
@ -171,9 +192,13 @@ class FeedbackTable:
|
|||
return None
|
||||
|
||||
def get_feedback_items(
|
||||
self, filter: dict = {}, skip: int = 0, limit: int = 30
|
||||
self,
|
||||
filter: dict = {},
|
||||
skip: int = 0,
|
||||
limit: int = 30,
|
||||
db: Optional[Session] = None,
|
||||
) -> FeedbackListResponse:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Feedback, User).join(User, Feedback.user_id == User.id)
|
||||
|
||||
if filter:
|
||||
|
|
@ -234,8 +259,8 @@ class FeedbackTable:
|
|||
|
||||
return FeedbackListResponse(items=feedbacks, total=total)
|
||||
|
||||
def get_all_feedbacks(self) -> list[FeedbackModel]:
|
||||
with get_db() as db:
|
||||
def get_all_feedbacks(self, db: Optional[Session] = None) -> list[FeedbackModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FeedbackModel.model_validate(feedback)
|
||||
for feedback in db.query(Feedback)
|
||||
|
|
@ -243,8 +268,110 @@ class FeedbackTable:
|
|||
.all()
|
||||
]
|
||||
|
||||
def get_feedbacks_by_type(self, type: str) -> list[FeedbackModel]:
|
||||
with get_db() as db:
|
||||
def get_all_feedback_ids(
|
||||
self, db: Optional[Session] = None
|
||||
) -> list[FeedbackIdResponse]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FeedbackIdResponse(
|
||||
id=row.id,
|
||||
user_id=row.user_id,
|
||||
created_at=row.created_at,
|
||||
updated_at=row.updated_at,
|
||||
)
|
||||
for row in db.query(
|
||||
Feedback.id,
|
||||
Feedback.user_id,
|
||||
Feedback.created_at,
|
||||
Feedback.updated_at,
|
||||
)
|
||||
.order_by(Feedback.updated_at.desc())
|
||||
.all()
|
||||
]
|
||||
|
||||
def get_feedbacks_for_leaderboard(
|
||||
self, db: Optional[Session] = None
|
||||
) -> list[LeaderboardFeedbackData]:
|
||||
"""Fetch only id and data for leaderboard computation (excludes snapshot/meta)."""
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
LeaderboardFeedbackData(id=row.id, data=row.data)
|
||||
for row in db.query(Feedback.id, Feedback.data).all()
|
||||
]
|
||||
|
||||
def get_model_evaluation_history(
|
||||
self, model_id: str, days: int = 30, db: Optional[Session] = None
|
||||
) -> list[ModelHistoryEntry]:
|
||||
"""
|
||||
Get daily wins/losses for a specific model over the past N days.
|
||||
If days=0, returns all time data starting from first feedback.
|
||||
Returns: [{"date": "2026-01-08", "won": 5, "lost": 2}, ...]
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from collections import defaultdict
|
||||
|
||||
with get_db_context(db) as db:
|
||||
if days == 0:
|
||||
# All time - no cutoff
|
||||
rows = db.query(Feedback.created_at, Feedback.data).all()
|
||||
else:
|
||||
cutoff = int(time.time()) - (days * 86400)
|
||||
rows = (
|
||||
db.query(Feedback.created_at, Feedback.data)
|
||||
.filter(Feedback.created_at >= cutoff)
|
||||
.all()
|
||||
)
|
||||
|
||||
daily_counts = defaultdict(lambda: {"won": 0, "lost": 0})
|
||||
first_date = None
|
||||
|
||||
for created_at, data in rows:
|
||||
if not data:
|
||||
continue
|
||||
if data.get("model_id") != model_id:
|
||||
continue
|
||||
|
||||
rating_str = str(data.get("rating", ""))
|
||||
if rating_str not in ("1", "-1"):
|
||||
continue
|
||||
|
||||
date_str = datetime.fromtimestamp(created_at).strftime("%Y-%m-%d")
|
||||
if rating_str == "1":
|
||||
daily_counts[date_str]["won"] += 1
|
||||
else:
|
||||
daily_counts[date_str]["lost"] += 1
|
||||
|
||||
# Track first date for this model
|
||||
if first_date is None or date_str < first_date:
|
||||
first_date = date_str
|
||||
|
||||
# Generate date range
|
||||
result = []
|
||||
today = datetime.now().date()
|
||||
|
||||
if days == 0 and first_date:
|
||||
# All time: start from first feedback date
|
||||
start_date = datetime.strptime(first_date, "%Y-%m-%d").date()
|
||||
num_days = (today - start_date).days + 1
|
||||
else:
|
||||
# Fixed range
|
||||
num_days = days
|
||||
start_date = today - timedelta(days=days - 1)
|
||||
|
||||
for i in range(num_days):
|
||||
d = start_date + timedelta(days=i)
|
||||
date_str = d.strftime("%Y-%m-%d")
|
||||
counts = daily_counts.get(date_str, {"won": 0, "lost": 0})
|
||||
result.append(
|
||||
ModelHistoryEntry(date=date_str, won=counts["won"], lost=counts["lost"])
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def get_feedbacks_by_type(
|
||||
self, type: str, db: Optional[Session] = None
|
||||
) -> list[FeedbackModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FeedbackModel.model_validate(feedback)
|
||||
for feedback in db.query(Feedback)
|
||||
|
|
@ -253,8 +380,10 @@ class FeedbackTable:
|
|||
.all()
|
||||
]
|
||||
|
||||
def get_feedbacks_by_user_id(self, user_id: str) -> list[FeedbackModel]:
|
||||
with get_db() as db:
|
||||
def get_feedbacks_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> list[FeedbackModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FeedbackModel.model_validate(feedback)
|
||||
for feedback in db.query(Feedback)
|
||||
|
|
@ -264,9 +393,9 @@ class FeedbackTable:
|
|||
]
|
||||
|
||||
def update_feedback_by_id(
|
||||
self, id: str, form_data: FeedbackForm
|
||||
self, id: str, form_data: FeedbackForm, db: Optional[Session] = None
|
||||
) -> Optional[FeedbackModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
feedback = db.query(Feedback).filter_by(id=id).first()
|
||||
if not feedback:
|
||||
return None
|
||||
|
|
@ -284,9 +413,13 @@ class FeedbackTable:
|
|||
return FeedbackModel.model_validate(feedback)
|
||||
|
||||
def update_feedback_by_id_and_user_id(
|
||||
self, id: str, user_id: str, form_data: FeedbackForm
|
||||
self,
|
||||
id: str,
|
||||
user_id: str,
|
||||
form_data: FeedbackForm,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[FeedbackModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
feedback = db.query(Feedback).filter_by(id=id, user_id=user_id).first()
|
||||
if not feedback:
|
||||
return None
|
||||
|
|
@ -303,8 +436,8 @@ class FeedbackTable:
|
|||
db.commit()
|
||||
return FeedbackModel.model_validate(feedback)
|
||||
|
||||
def delete_feedback_by_id(self, id: str) -> bool:
|
||||
with get_db() as db:
|
||||
def delete_feedback_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
feedback = db.query(Feedback).filter_by(id=id).first()
|
||||
if not feedback:
|
||||
return False
|
||||
|
|
@ -312,8 +445,10 @@ class FeedbackTable:
|
|||
db.commit()
|
||||
return True
|
||||
|
||||
def delete_feedback_by_id_and_user_id(self, id: str, user_id: str) -> bool:
|
||||
with get_db() as db:
|
||||
def delete_feedback_by_id_and_user_id(
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
feedback = db.query(Feedback).filter_by(id=id, user_id=user_id).first()
|
||||
if not feedback:
|
||||
return False
|
||||
|
|
@ -321,8 +456,10 @@ class FeedbackTable:
|
|||
db.commit()
|
||||
return True
|
||||
|
||||
def delete_feedbacks_by_user_id(self, user_id: str) -> bool:
|
||||
with get_db() as db:
|
||||
def delete_feedbacks_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
feedbacks = db.query(Feedback).filter_by(user_id=user_id).all()
|
||||
if not feedbacks:
|
||||
return False
|
||||
|
|
@ -331,8 +468,8 @@ class FeedbackTable:
|
|||
db.commit()
|
||||
return True
|
||||
|
||||
def delete_all_feedbacks(self) -> bool:
|
||||
with get_db() as db:
|
||||
def delete_all_feedbacks(self, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
feedbacks = db.query(Feedback).all()
|
||||
if not feedbacks:
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import logging
|
|||
import time
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.internal.db import Base, JSONField, get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import BigInteger, Column, String, Text, JSON
|
||||
|
||||
|
|
@ -108,8 +109,10 @@ class FileListResponse(BaseModel):
|
|||
|
||||
|
||||
class FilesTable:
|
||||
def insert_new_file(self, user_id: str, form_data: FileForm) -> Optional[FileModel]:
|
||||
with get_db() as db:
|
||||
def insert_new_file(
|
||||
self, user_id: str, form_data: FileForm, db: Optional[Session] = None
|
||||
) -> Optional[FileModel]:
|
||||
with get_db_context(db) as db:
|
||||
file = FileModel(
|
||||
**{
|
||||
**form_data.model_dump(),
|
||||
|
|
@ -132,16 +135,23 @@ class FilesTable:
|
|||
log.exception(f"Error inserting a new file: {e}")
|
||||
return None
|
||||
|
||||
def get_file_by_id(self, id: str) -> Optional[FileModel]:
|
||||
with get_db() as db:
|
||||
try:
|
||||
file = db.get(File, id)
|
||||
return FileModel.model_validate(file)
|
||||
except Exception:
|
||||
return None
|
||||
def get_file_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[FileModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
file = db.get(File, id)
|
||||
return FileModel.model_validate(file)
|
||||
except Exception:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_file_by_id_and_user_id(self, id: str, user_id: str) -> Optional[FileModel]:
|
||||
with get_db() as db:
|
||||
def get_file_by_id_and_user_id(
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[FileModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
file = db.query(File).filter_by(id=id, user_id=user_id).first()
|
||||
if file:
|
||||
|
|
@ -151,8 +161,10 @@ class FilesTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def get_file_metadata_by_id(self, id: str) -> Optional[FileMetadataResponse]:
|
||||
with get_db() as db:
|
||||
def get_file_metadata_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[FileMetadataResponse]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
file = db.get(File, id)
|
||||
return FileMetadataResponse(
|
||||
|
|
@ -165,12 +177,14 @@ class FilesTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def get_files(self) -> list[FileModel]:
|
||||
with get_db() as db:
|
||||
def get_files(self, db: Optional[Session] = None) -> list[FileModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [FileModel.model_validate(file) for file in db.query(File).all()]
|
||||
|
||||
def check_access_by_user_id(self, id, user_id, permission="write") -> bool:
|
||||
file = self.get_file_by_id(id)
|
||||
def check_access_by_user_id(
|
||||
self, id, user_id, permission="write", db: Optional[Session] = None
|
||||
) -> bool:
|
||||
file = self.get_file_by_id(id, db=db)
|
||||
if not file:
|
||||
return False
|
||||
if file.user_id == user_id:
|
||||
|
|
@ -178,8 +192,10 @@ class FilesTable:
|
|||
# Implement additional access control logic here as needed
|
||||
return False
|
||||
|
||||
def get_files_by_ids(self, ids: list[str]) -> list[FileModel]:
|
||||
with get_db() as db:
|
||||
def get_files_by_ids(
|
||||
self, ids: list[str], db: Optional[Session] = None
|
||||
) -> list[FileModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FileModel.model_validate(file)
|
||||
for file in db.query(File)
|
||||
|
|
@ -188,8 +204,10 @@ class FilesTable:
|
|||
.all()
|
||||
]
|
||||
|
||||
def get_file_metadatas_by_ids(self, ids: list[str]) -> list[FileMetadataResponse]:
|
||||
with get_db() as db:
|
||||
def get_file_metadatas_by_ids(
|
||||
self, ids: list[str], db: Optional[Session] = None
|
||||
) -> list[FileMetadataResponse]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FileMetadataResponse(
|
||||
id=file.id,
|
||||
|
|
@ -206,17 +224,81 @@ class FilesTable:
|
|||
.all()
|
||||
]
|
||||
|
||||
def get_files_by_user_id(self, user_id: str) -> list[FileModel]:
|
||||
with get_db() as db:
|
||||
def get_files_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> list[FileModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FileModel.model_validate(file)
|
||||
for file in db.query(File).filter_by(user_id=user_id).all()
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _glob_to_like_pattern(glob: str) -> str:
|
||||
"""
|
||||
Convert a glob/fnmatch pattern to a SQL LIKE pattern.
|
||||
|
||||
Escapes SQL special characters and converts glob wildcards:
|
||||
- `*` becomes `%` (match any sequence of characters)
|
||||
- `?` becomes `_` (match exactly one character)
|
||||
|
||||
Args:
|
||||
glob: A glob pattern (e.g., "*.txt", "file?.doc")
|
||||
|
||||
Returns:
|
||||
A SQL LIKE compatible pattern with proper escaping.
|
||||
"""
|
||||
# Escape SQL special characters first, then convert glob wildcards
|
||||
pattern = glob.replace("\\", "\\\\")
|
||||
pattern = pattern.replace("%", "\\%")
|
||||
pattern = pattern.replace("_", "\\_")
|
||||
pattern = pattern.replace("*", "%")
|
||||
pattern = pattern.replace("?", "_")
|
||||
return pattern
|
||||
|
||||
def search_files(
|
||||
self,
|
||||
user_id: Optional[str] = None,
|
||||
filename: str = "*",
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Optional[Session] = None,
|
||||
) -> list[FileModel]:
|
||||
"""
|
||||
Search files with glob pattern matching, optional user filter, and pagination.
|
||||
|
||||
Args:
|
||||
user_id: Filter by user ID. If None, returns files for all users.
|
||||
filename: Glob pattern to match filenames (e.g., "*.txt"). Default "*" matches all.
|
||||
skip: Number of results to skip for pagination.
|
||||
limit: Maximum number of results to return.
|
||||
db: Optional database session.
|
||||
|
||||
Returns:
|
||||
List of matching FileModel objects, ordered by updated_at descending.
|
||||
"""
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(File)
|
||||
|
||||
if user_id:
|
||||
query = query.filter_by(user_id=user_id)
|
||||
|
||||
pattern = self._glob_to_like_pattern(filename)
|
||||
if pattern != "%":
|
||||
query = query.filter(File.filename.ilike(pattern, escape="\\"))
|
||||
|
||||
return [
|
||||
FileModel.model_validate(file)
|
||||
for file in query.order_by(File.updated_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
]
|
||||
|
||||
def update_file_by_id(
|
||||
self, id: str, form_data: FileUpdateForm
|
||||
self, id: str, form_data: FileUpdateForm, db: Optional[Session] = None
|
||||
) -> Optional[FileModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
file = db.query(File).filter_by(id=id).first()
|
||||
|
||||
|
|
@ -236,8 +318,10 @@ class FilesTable:
|
|||
log.exception(f"Error updating file completely by id: {e}")
|
||||
return None
|
||||
|
||||
def update_file_hash_by_id(self, id: str, hash: str) -> Optional[FileModel]:
|
||||
with get_db() as db:
|
||||
def update_file_hash_by_id(
|
||||
self, id: str, hash: Optional[str], db: Optional[Session] = None
|
||||
) -> Optional[FileModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
file = db.query(File).filter_by(id=id).first()
|
||||
file.hash = hash
|
||||
|
|
@ -248,8 +332,10 @@ class FilesTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def update_file_data_by_id(self, id: str, data: dict) -> Optional[FileModel]:
|
||||
with get_db() as db:
|
||||
def update_file_data_by_id(
|
||||
self, id: str, data: dict, db: Optional[Session] = None
|
||||
) -> Optional[FileModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
file = db.query(File).filter_by(id=id).first()
|
||||
file.data = {**(file.data if file.data else {}), **data}
|
||||
|
|
@ -260,8 +346,10 @@ class FilesTable:
|
|||
|
||||
return None
|
||||
|
||||
def update_file_metadata_by_id(self, id: str, meta: dict) -> Optional[FileModel]:
|
||||
with get_db() as db:
|
||||
def update_file_metadata_by_id(
|
||||
self, id: str, meta: dict, db: Optional[Session] = None
|
||||
) -> Optional[FileModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
file = db.query(File).filter_by(id=id).first()
|
||||
file.meta = {**(file.meta if file.meta else {}), **meta}
|
||||
|
|
@ -271,8 +359,10 @@ class FilesTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def delete_file_by_id(self, id: str) -> bool:
|
||||
with get_db() as db:
|
||||
return False
|
||||
|
||||
def delete_file_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
db.query(File).filter_by(id=id).delete()
|
||||
db.commit()
|
||||
|
|
@ -281,8 +371,8 @@ class FilesTable:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_all_files(self) -> bool:
|
||||
with get_db() as db:
|
||||
def delete_all_files(self, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
db.query(File).delete()
|
||||
db.commit()
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ import re
|
|||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import BigInteger, Column, Text, JSON, Boolean, func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from open_webui.internal.db import Base, get_db
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -83,9 +84,13 @@ class FolderUpdateForm(BaseModel):
|
|||
|
||||
class FolderTable:
|
||||
def insert_new_folder(
|
||||
self, user_id: str, form_data: FolderForm, parent_id: Optional[str] = None
|
||||
self,
|
||||
user_id: str,
|
||||
form_data: FolderForm,
|
||||
parent_id: Optional[str] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[FolderModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
id = str(uuid.uuid4())
|
||||
folder = FolderModel(
|
||||
**{
|
||||
|
|
@ -111,10 +116,10 @@ class FolderTable:
|
|||
return None
|
||||
|
||||
def get_folder_by_id_and_user_id(
|
||||
self, id: str, user_id: str
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[FolderModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
|
||||
|
||||
if not folder:
|
||||
|
|
@ -125,15 +130,15 @@ class FolderTable:
|
|||
return None
|
||||
|
||||
def get_children_folders_by_id_and_user_id(
|
||||
self, id: str, user_id: str
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[list[FolderModel]]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
folders = []
|
||||
|
||||
def get_children(folder):
|
||||
children = self.get_folders_by_parent_id_and_user_id(
|
||||
folder.id, user_id
|
||||
folder.id, user_id, db=db
|
||||
)
|
||||
for child in children:
|
||||
get_children(child)
|
||||
|
|
@ -148,18 +153,24 @@ class FolderTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def get_folders_by_user_id(self, user_id: str) -> list[FolderModel]:
|
||||
with get_db() as db:
|
||||
def get_folders_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> list[FolderModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FolderModel.model_validate(folder)
|
||||
for folder in db.query(Folder).filter_by(user_id=user_id).all()
|
||||
]
|
||||
|
||||
def get_folder_by_parent_id_and_user_id_and_name(
|
||||
self, parent_id: Optional[str], user_id: str, name: str
|
||||
self,
|
||||
parent_id: Optional[str],
|
||||
user_id: str,
|
||||
name: str,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[FolderModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
# Check if folder exists
|
||||
folder = (
|
||||
db.query(Folder)
|
||||
|
|
@ -177,9 +188,9 @@ class FolderTable:
|
|||
return None
|
||||
|
||||
def get_folders_by_parent_id_and_user_id(
|
||||
self, parent_id: Optional[str], user_id: str
|
||||
self, parent_id: Optional[str], user_id: str, db: Optional[Session] = None
|
||||
) -> list[FolderModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FolderModel.model_validate(folder)
|
||||
for folder in db.query(Folder)
|
||||
|
|
@ -192,9 +203,10 @@ class FolderTable:
|
|||
id: str,
|
||||
user_id: str,
|
||||
parent_id: str,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[FolderModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
|
||||
|
||||
if not folder:
|
||||
|
|
@ -211,10 +223,14 @@ class FolderTable:
|
|||
return
|
||||
|
||||
def update_folder_by_id_and_user_id(
|
||||
self, id: str, user_id: str, form_data: FolderUpdateForm
|
||||
self,
|
||||
id: str,
|
||||
user_id: str,
|
||||
form_data: FolderUpdateForm,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[FolderModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
|
||||
|
||||
if not folder:
|
||||
|
|
@ -257,10 +273,10 @@ class FolderTable:
|
|||
return
|
||||
|
||||
def update_folder_is_expanded_by_id_and_user_id(
|
||||
self, id: str, user_id: str, is_expanded: bool
|
||||
self, id: str, user_id: str, is_expanded: bool, db: Optional[Session] = None
|
||||
) -> Optional[FolderModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
|
||||
|
||||
if not folder:
|
||||
|
|
@ -276,10 +292,12 @@ class FolderTable:
|
|||
log.error(f"update_folder: {e}")
|
||||
return
|
||||
|
||||
def delete_folder_by_id_and_user_id(self, id: str, user_id: str) -> list[str]:
|
||||
def delete_folder_by_id_and_user_id(
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> list[str]:
|
||||
try:
|
||||
folder_ids = []
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
|
||||
if not folder:
|
||||
return folder_ids
|
||||
|
|
@ -289,7 +307,7 @@ class FolderTable:
|
|||
# Delete all children folders
|
||||
def delete_children(folder):
|
||||
folder_children = self.get_folders_by_parent_id_and_user_id(
|
||||
folder.id, user_id
|
||||
folder.id, user_id, db=db
|
||||
)
|
||||
for folder_child in folder_children:
|
||||
|
||||
|
|
@ -314,7 +332,7 @@ class FolderTable:
|
|||
return name.strip().lower()
|
||||
|
||||
def search_folders_by_names(
|
||||
self, user_id: str, queries: list[str]
|
||||
self, user_id: str, queries: list[str], db: Optional[Session] = None
|
||||
) -> list[FolderModel]:
|
||||
"""
|
||||
Search for folders for a user where the name matches any of the queries, treating _ and space as equivalent, case-insensitive.
|
||||
|
|
@ -324,7 +342,7 @@ class FolderTable:
|
|||
return []
|
||||
|
||||
results = {}
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
folders = db.query(Folder).filter_by(user_id=user_id).all()
|
||||
for folder in folders:
|
||||
if self.normalize_folder_name(folder.name) in normalized_queries:
|
||||
|
|
@ -332,7 +350,7 @@ class FolderTable:
|
|||
|
||||
# get children folders
|
||||
children = self.get_children_folders_by_id_and_user_id(
|
||||
folder.id, user_id
|
||||
folder.id, user_id, db=db
|
||||
)
|
||||
for child in children:
|
||||
results[child.id] = child
|
||||
|
|
@ -345,14 +363,14 @@ class FolderTable:
|
|||
return results
|
||||
|
||||
def search_folders_by_name_contains(
|
||||
self, user_id: str, query: str
|
||||
self, user_id: str, query: str, db: Optional[Session] = None
|
||||
) -> list[FolderModel]:
|
||||
"""
|
||||
Partial match: normalized name contains (as substring) the normalized query.
|
||||
"""
|
||||
normalized_query = self.normalize_folder_name(query)
|
||||
results = []
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
folders = db.query(Folder).filter_by(user_id=user_id).all()
|
||||
for folder in folders:
|
||||
norm_name = self.normalize_folder_name(folder.name)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import logging
|
|||
import time
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.internal.db import Base, JSONField, get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
from open_webui.models.users import Users, UserModel
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import BigInteger, Boolean, Column, String, Text, Index
|
||||
|
|
@ -103,7 +104,11 @@ class FunctionValves(BaseModel):
|
|||
|
||||
class FunctionsTable:
|
||||
def insert_new_function(
|
||||
self, user_id: str, type: str, form_data: FunctionForm
|
||||
self,
|
||||
user_id: str,
|
||||
type: str,
|
||||
form_data: FunctionForm,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[FunctionModel]:
|
||||
function = FunctionModel(
|
||||
**{
|
||||
|
|
@ -116,7 +121,7 @@ class FunctionsTable:
|
|||
)
|
||||
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
result = Function(**function.model_dump())
|
||||
db.add(result)
|
||||
db.commit()
|
||||
|
|
@ -130,11 +135,14 @@ class FunctionsTable:
|
|||
return None
|
||||
|
||||
def sync_functions(
|
||||
self, user_id: str, functions: list[FunctionWithValvesModel]
|
||||
self,
|
||||
user_id: str,
|
||||
functions: list[FunctionWithValvesModel],
|
||||
db: Optional[Session] = None,
|
||||
) -> list[FunctionWithValvesModel]:
|
||||
# Synchronize functions for a user by updating existing ones, inserting new ones, and removing those that are no longer present.
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
# Get existing functions
|
||||
existing_functions = db.query(Function).all()
|
||||
existing_ids = {func.id for func in existing_functions}
|
||||
|
|
@ -177,18 +185,20 @@ class FunctionsTable:
|
|||
log.exception(f"Error syncing functions for user {user_id}: {e}")
|
||||
return []
|
||||
|
||||
def get_function_by_id(self, id: str) -> Optional[FunctionModel]:
|
||||
def get_function_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[FunctionModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
function = db.get(Function, id)
|
||||
return FunctionModel.model_validate(function)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_functions(
|
||||
self, active_only=False, include_valves=False
|
||||
self, active_only=False, include_valves=False, db: Optional[Session] = None
|
||||
) -> list[FunctionModel | FunctionWithValvesModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
if active_only:
|
||||
functions = db.query(Function).filter_by(is_active=True).all()
|
||||
|
||||
|
|
@ -205,12 +215,14 @@ class FunctionsTable:
|
|||
FunctionModel.model_validate(function) for function in functions
|
||||
]
|
||||
|
||||
def get_function_list(self) -> list[FunctionUserResponse]:
|
||||
with get_db() as db:
|
||||
def get_function_list(
|
||||
self, db: Optional[Session] = None
|
||||
) -> list[FunctionUserResponse]:
|
||||
with get_db_context(db) as db:
|
||||
functions = db.query(Function).order_by(Function.updated_at.desc()).all()
|
||||
user_ids = list(set(func.user_id for func in functions))
|
||||
|
||||
users = Users.get_users_by_user_ids(user_ids) if user_ids else []
|
||||
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
|
||||
users_dict = {user.id: user for user in users}
|
||||
|
||||
return [
|
||||
|
|
@ -228,9 +240,9 @@ class FunctionsTable:
|
|||
]
|
||||
|
||||
def get_functions_by_type(
|
||||
self, type: str, active_only=False
|
||||
self, type: str, active_only=False, db: Optional[Session] = None
|
||||
) -> list[FunctionModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
if active_only:
|
||||
return [
|
||||
FunctionModel.model_validate(function)
|
||||
|
|
@ -244,8 +256,10 @@ class FunctionsTable:
|
|||
for function in db.query(Function).filter_by(type=type).all()
|
||||
]
|
||||
|
||||
def get_global_filter_functions(self) -> list[FunctionModel]:
|
||||
with get_db() as db:
|
||||
def get_global_filter_functions(
|
||||
self, db: Optional[Session] = None
|
||||
) -> list[FunctionModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FunctionModel.model_validate(function)
|
||||
for function in db.query(Function)
|
||||
|
|
@ -253,8 +267,10 @@ class FunctionsTable:
|
|||
.all()
|
||||
]
|
||||
|
||||
def get_global_action_functions(self) -> list[FunctionModel]:
|
||||
with get_db() as db:
|
||||
def get_global_action_functions(
|
||||
self, db: Optional[Session] = None
|
||||
) -> list[FunctionModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FunctionModel.model_validate(function)
|
||||
for function in db.query(Function)
|
||||
|
|
@ -262,8 +278,10 @@ class FunctionsTable:
|
|||
.all()
|
||||
]
|
||||
|
||||
def get_function_valves_by_id(self, id: str) -> Optional[dict]:
|
||||
with get_db() as db:
|
||||
def get_function_valves_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[dict]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
function = db.get(Function, id)
|
||||
return function.valves if function.valves else {}
|
||||
|
|
@ -272,23 +290,23 @@ class FunctionsTable:
|
|||
return None
|
||||
|
||||
def update_function_valves_by_id(
|
||||
self, id: str, valves: dict
|
||||
self, id: str, valves: dict, db: Optional[Session] = None
|
||||
) -> Optional[FunctionValves]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
function = db.get(Function, id)
|
||||
function.valves = valves
|
||||
function.updated_at = int(time.time())
|
||||
db.commit()
|
||||
db.refresh(function)
|
||||
return self.get_function_by_id(id)
|
||||
return self.get_function_by_id(id, db=db)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_function_metadata_by_id(
|
||||
self, id: str, metadata: dict
|
||||
self, id: str, metadata: dict, db: Optional[Session] = None
|
||||
) -> Optional[FunctionModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
function = db.get(Function, id)
|
||||
|
||||
|
|
@ -301,7 +319,7 @@ class FunctionsTable:
|
|||
function.updated_at = int(time.time())
|
||||
db.commit()
|
||||
db.refresh(function)
|
||||
return self.get_function_by_id(id)
|
||||
return self.get_function_by_id(id, db=db)
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
|
|
@ -309,10 +327,10 @@ class FunctionsTable:
|
|||
return None
|
||||
|
||||
def get_user_valves_by_id_and_user_id(
|
||||
self, id: str, user_id: str
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[dict]:
|
||||
try:
|
||||
user = Users.get_user_by_id(user_id)
|
||||
user = Users.get_user_by_id(user_id, db=db)
|
||||
user_settings = user.settings.model_dump() if user.settings else {}
|
||||
|
||||
# Check if user has "functions" and "valves" settings
|
||||
|
|
@ -327,10 +345,10 @@ class FunctionsTable:
|
|||
return None
|
||||
|
||||
def update_user_valves_by_id_and_user_id(
|
||||
self, id: str, user_id: str, valves: dict
|
||||
self, id: str, user_id: str, valves: dict, db: Optional[Session] = None
|
||||
) -> Optional[dict]:
|
||||
try:
|
||||
user = Users.get_user_by_id(user_id)
|
||||
user = Users.get_user_by_id(user_id, db=db)
|
||||
user_settings = user.settings.model_dump() if user.settings else {}
|
||||
|
||||
# Check if user has "functions" and "valves" settings
|
||||
|
|
@ -342,7 +360,7 @@ class FunctionsTable:
|
|||
user_settings["functions"]["valves"][id] = valves
|
||||
|
||||
# Update the user settings in the database
|
||||
Users.update_user_by_id(user_id, {"settings": user_settings})
|
||||
Users.update_user_by_id(user_id, {"settings": user_settings}, db=db)
|
||||
|
||||
return user_settings["functions"]["valves"][id]
|
||||
except Exception as e:
|
||||
|
|
@ -351,8 +369,10 @@ class FunctionsTable:
|
|||
)
|
||||
return None
|
||||
|
||||
def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]:
|
||||
with get_db() as db:
|
||||
def update_function_by_id(
|
||||
self, id: str, updated: dict, db: Optional[Session] = None
|
||||
) -> Optional[FunctionModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
db.query(Function).filter_by(id=id).update(
|
||||
{
|
||||
|
|
@ -361,12 +381,12 @@ class FunctionsTable:
|
|||
}
|
||||
)
|
||||
db.commit()
|
||||
return self.get_function_by_id(id)
|
||||
return self.get_function_by_id(id, db=db)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def deactivate_all_functions(self) -> Optional[bool]:
|
||||
with get_db() as db:
|
||||
def deactivate_all_functions(self, db: Optional[Session] = None) -> Optional[bool]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
db.query(Function).update(
|
||||
{
|
||||
|
|
@ -379,8 +399,8 @@ class FunctionsTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def delete_function_by_id(self, id: str) -> bool:
|
||||
with get_db() as db:
|
||||
def delete_function_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
db.query(Function).filter_by(id=id).delete()
|
||||
db.commit()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import time
|
|||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from open_webui.internal.db import Base, get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
|
||||
from open_webui.models.files import FileMetadataResponse
|
||||
|
||||
|
|
@ -120,9 +121,9 @@ class GroupListResponse(BaseModel):
|
|||
|
||||
class GroupTable:
|
||||
def insert_new_group(
|
||||
self, user_id: str, form_data: GroupForm
|
||||
self, user_id: str, form_data: GroupForm, db: Optional[Session] = None
|
||||
) -> Optional[GroupModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
group = GroupModel(
|
||||
**{
|
||||
**form_data.model_dump(exclude_none=True),
|
||||
|
|
@ -146,54 +147,84 @@ class GroupTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def get_all_groups(self) -> list[GroupModel]:
|
||||
with get_db() as db:
|
||||
def get_all_groups(self, db: Optional[Session] = None) -> list[GroupModel]:
|
||||
with get_db_context(db) as db:
|
||||
groups = db.query(Group).order_by(Group.updated_at.desc()).all()
|
||||
return [GroupModel.model_validate(group) for group in groups]
|
||||
|
||||
def get_groups(self, filter) -> list[GroupResponse]:
|
||||
with get_db() as db:
|
||||
def get_groups(self, filter, db: Optional[Session] = None) -> list[GroupResponse]:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Group)
|
||||
|
||||
if filter:
|
||||
if "query" in filter:
|
||||
query = query.filter(Group.name.ilike(f"%{filter['query']}%"))
|
||||
if "member_id" in filter:
|
||||
query = query.join(
|
||||
GroupMember, GroupMember.group_id == Group.id
|
||||
).filter(GroupMember.user_id == filter["member_id"])
|
||||
|
||||
# When share filter is present, member check is handled in the share logic
|
||||
if "share" in filter:
|
||||
share_value = filter["share"]
|
||||
json_share = Group.data["config"]["share"].as_boolean()
|
||||
member_id = filter.get("member_id")
|
||||
json_share = Group.data["config"]["share"]
|
||||
json_share_bool = json_share.as_boolean()
|
||||
json_share_str = json_share.as_string()
|
||||
|
||||
if share_value:
|
||||
query = query.filter(
|
||||
or_(
|
||||
Group.data.is_(None),
|
||||
json_share.is_(None),
|
||||
json_share == True,
|
||||
)
|
||||
# Groups open to anyone: data is null, share is null, or share is true
|
||||
anyone_can_share = or_(
|
||||
Group.data.is_(None),
|
||||
json_share_bool.is_(None),
|
||||
json_share_bool == True,
|
||||
)
|
||||
|
||||
if member_id:
|
||||
# Also include member-only groups where user is a member
|
||||
member_groups_subq = (
|
||||
db.query(GroupMember.group_id)
|
||||
.filter(GroupMember.user_id == member_id)
|
||||
.subquery()
|
||||
)
|
||||
members_only_and_is_member = and_(
|
||||
json_share_str == "members",
|
||||
Group.id.in_(member_groups_subq),
|
||||
)
|
||||
query = query.filter(
|
||||
or_(anyone_can_share, members_only_and_is_member)
|
||||
)
|
||||
else:
|
||||
query = query.filter(anyone_can_share)
|
||||
else:
|
||||
query = query.filter(
|
||||
and_(Group.data.isnot(None), json_share == False)
|
||||
and_(Group.data.isnot(None), json_share_bool == False)
|
||||
)
|
||||
|
||||
else:
|
||||
# Only apply member_id filter when share filter is NOT present
|
||||
if "member_id" in filter:
|
||||
query = query.join(
|
||||
GroupMember, GroupMember.group_id == Group.id
|
||||
).filter(GroupMember.user_id == filter["member_id"])
|
||||
|
||||
groups = query.order_by(Group.updated_at.desc()).all()
|
||||
return [
|
||||
GroupResponse.model_validate(
|
||||
{
|
||||
**GroupModel.model_validate(group).model_dump(),
|
||||
"member_count": self.get_group_member_count_by_id(group.id),
|
||||
"member_count": self.get_group_member_count_by_id(
|
||||
group.id, db=db
|
||||
),
|
||||
}
|
||||
)
|
||||
for group in groups
|
||||
]
|
||||
|
||||
def search_groups(
|
||||
self, filter: Optional[dict] = None, skip: int = 0, limit: int = 30
|
||||
self,
|
||||
filter: Optional[dict] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 30,
|
||||
db: Optional[Session] = None,
|
||||
) -> GroupListResponse:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Group)
|
||||
|
||||
if filter:
|
||||
|
|
@ -220,15 +251,17 @@ class GroupTable:
|
|||
"items": [
|
||||
GroupResponse.model_validate(
|
||||
**GroupModel.model_validate(group).model_dump(),
|
||||
member_count=self.get_group_member_count_by_id(group.id),
|
||||
member_count=self.get_group_member_count_by_id(group.id, db=db),
|
||||
)
|
||||
for group in groups
|
||||
],
|
||||
"total": total,
|
||||
}
|
||||
|
||||
def get_groups_by_member_id(self, user_id: str) -> list[GroupModel]:
|
||||
with get_db() as db:
|
||||
def get_groups_by_member_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> list[GroupModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
GroupModel.model_validate(group)
|
||||
for group in db.query(Group)
|
||||
|
|
@ -238,16 +271,41 @@ class GroupTable:
|
|||
.all()
|
||||
]
|
||||
|
||||
def get_group_by_id(self, id: str) -> Optional[GroupModel]:
|
||||
def get_groups_by_member_ids(
|
||||
self, user_ids: list[str], db: Optional[Session] = None
|
||||
) -> dict[str, list[GroupModel]]:
|
||||
"""Fetch groups for multiple users in a single query to avoid N+1."""
|
||||
with get_db_context(db) as db:
|
||||
# Query GroupMember joined with Group, filtering by user_ids
|
||||
results = (
|
||||
db.query(GroupMember.user_id, Group)
|
||||
.join(Group, Group.id == GroupMember.group_id)
|
||||
.filter(GroupMember.user_id.in_(user_ids))
|
||||
.order_by(Group.updated_at.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
# Group groups by user_id
|
||||
user_groups: dict[str, list[GroupModel]] = {uid: [] for uid in user_ids}
|
||||
for user_id, group in results:
|
||||
user_groups[user_id].append(GroupModel.model_validate(group))
|
||||
|
||||
return user_groups
|
||||
|
||||
def get_group_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[GroupModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
group = db.query(Group).filter_by(id=id).first()
|
||||
return GroupModel.model_validate(group) if group else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_group_user_ids_by_id(self, id: str) -> Optional[list[str]]:
|
||||
with get_db() as db:
|
||||
def get_group_user_ids_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[list[str]]:
|
||||
with get_db_context(db) as db:
|
||||
members = (
|
||||
db.query(GroupMember.user_id).filter(GroupMember.group_id == id).all()
|
||||
)
|
||||
|
|
@ -257,8 +315,10 @@ class GroupTable:
|
|||
|
||||
return [m[0] for m in members]
|
||||
|
||||
def get_group_user_ids_by_ids(self, group_ids: list[str]) -> dict[str, list[str]]:
|
||||
with get_db() as db:
|
||||
def get_group_user_ids_by_ids(
|
||||
self, group_ids: list[str], db: Optional[Session] = None
|
||||
) -> dict[str, list[str]]:
|
||||
with get_db_context(db) as db:
|
||||
members = (
|
||||
db.query(GroupMember.group_id, GroupMember.user_id)
|
||||
.filter(GroupMember.group_id.in_(group_ids))
|
||||
|
|
@ -274,8 +334,10 @@ class GroupTable:
|
|||
|
||||
return group_user_ids
|
||||
|
||||
def set_group_user_ids_by_id(self, group_id: str, user_ids: list[str]) -> None:
|
||||
with get_db() as db:
|
||||
def set_group_user_ids_by_id(
|
||||
self, group_id: str, user_ids: list[str], db: Optional[Session] = None
|
||||
) -> None:
|
||||
with get_db_context(db) as db:
|
||||
# Delete existing members
|
||||
db.query(GroupMember).filter(GroupMember.group_id == group_id).delete()
|
||||
|
||||
|
|
@ -295,8 +357,10 @@ class GroupTable:
|
|||
db.add_all(new_members)
|
||||
db.commit()
|
||||
|
||||
def get_group_member_count_by_id(self, id: str) -> int:
|
||||
with get_db() as db:
|
||||
def get_group_member_count_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> int:
|
||||
with get_db_context(db) as db:
|
||||
count = (
|
||||
db.query(func.count(GroupMember.user_id))
|
||||
.filter(GroupMember.group_id == id)
|
||||
|
|
@ -305,10 +369,14 @@ class GroupTable:
|
|||
return count if count else 0
|
||||
|
||||
def update_group_by_id(
|
||||
self, id: str, form_data: GroupUpdateForm, overwrite: bool = False
|
||||
self,
|
||||
id: str,
|
||||
form_data: GroupUpdateForm,
|
||||
overwrite: bool = False,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[GroupModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Group).filter_by(id=id).update(
|
||||
{
|
||||
**form_data.model_dump(exclude_none=True),
|
||||
|
|
@ -316,22 +384,22 @@ class GroupTable:
|
|||
}
|
||||
)
|
||||
db.commit()
|
||||
return self.get_group_by_id(id=id)
|
||||
return self.get_group_by_id(id=id, db=db)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
return None
|
||||
|
||||
def delete_group_by_id(self, id: str) -> bool:
|
||||
def delete_group_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Group).filter_by(id=id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_all_groups(self) -> bool:
|
||||
with get_db() as db:
|
||||
def delete_all_groups(self, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
db.query(Group).delete()
|
||||
db.commit()
|
||||
|
|
@ -340,8 +408,10 @@ class GroupTable:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def remove_user_from_all_groups(self, user_id: str) -> bool:
|
||||
with get_db() as db:
|
||||
def remove_user_from_all_groups(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
# Find all groups the user belongs to
|
||||
groups = (
|
||||
|
|
@ -369,16 +439,16 @@ class GroupTable:
|
|||
return False
|
||||
|
||||
def create_groups_by_group_names(
|
||||
self, user_id: str, group_names: list[str]
|
||||
self, user_id: str, group_names: list[str], db: Optional[Session] = None
|
||||
) -> list[GroupModel]:
|
||||
|
||||
# check for existing groups
|
||||
existing_groups = self.get_all_groups()
|
||||
existing_groups = self.get_all_groups(db=db)
|
||||
existing_group_names = {group.name for group in existing_groups}
|
||||
|
||||
new_groups = []
|
||||
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
for group_name in group_names:
|
||||
if group_name not in existing_group_names:
|
||||
new_group = GroupModel(
|
||||
|
|
@ -400,8 +470,10 @@ class GroupTable:
|
|||
continue
|
||||
return new_groups
|
||||
|
||||
def sync_groups_by_group_names(self, user_id: str, group_names: list[str]) -> bool:
|
||||
with get_db() as db:
|
||||
def sync_groups_by_group_names(
|
||||
self, user_id: str, group_names: list[str], db: Optional[Session] = None
|
||||
) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
now = int(time.time())
|
||||
|
||||
|
|
@ -461,10 +533,13 @@ class GroupTable:
|
|||
return False
|
||||
|
||||
def add_users_to_group(
|
||||
self, id: str, user_ids: Optional[list[str]] = None
|
||||
self,
|
||||
id: str,
|
||||
user_ids: Optional[list[str]] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[GroupModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
group = db.query(Group).filter_by(id=id).first()
|
||||
if not group:
|
||||
return None
|
||||
|
|
@ -499,10 +574,13 @@ class GroupTable:
|
|||
return None
|
||||
|
||||
def remove_users_from_group(
|
||||
self, id: str, user_ids: Optional[list[str]] = None
|
||||
self,
|
||||
id: str,
|
||||
user_ids: Optional[list[str]] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[GroupModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
group = db.query(Group).filter_by(id=id).first()
|
||||
if not group:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import time
|
|||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from open_webui.internal.db import Base, get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
|
||||
from open_webui.models.files import (
|
||||
File,
|
||||
|
|
@ -157,9 +158,9 @@ class KnowledgeFileListResponse(BaseModel):
|
|||
|
||||
class KnowledgeTable:
|
||||
def insert_new_knowledge(
|
||||
self, user_id: str, form_data: KnowledgeForm
|
||||
self, user_id: str, form_data: KnowledgeForm, db: Optional[Session] = None
|
||||
) -> Optional[KnowledgeModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
knowledge = KnowledgeModel(
|
||||
**{
|
||||
**form_data.model_dump(),
|
||||
|
|
@ -183,15 +184,15 @@ class KnowledgeTable:
|
|||
return None
|
||||
|
||||
def get_knowledge_bases(
|
||||
self, skip: int = 0, limit: int = 30
|
||||
self, skip: int = 0, limit: int = 30, db: Optional[Session] = None
|
||||
) -> list[KnowledgeUserModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
all_knowledge = (
|
||||
db.query(Knowledge).order_by(Knowledge.updated_at.desc()).all()
|
||||
)
|
||||
user_ids = list(set(knowledge.user_id for knowledge in all_knowledge))
|
||||
|
||||
users = Users.get_users_by_user_ids(user_ids) if user_ids else []
|
||||
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
|
||||
users_dict = {user.id: user for user in users}
|
||||
|
||||
knowledge_bases = []
|
||||
|
|
@ -208,10 +209,15 @@ class KnowledgeTable:
|
|||
return knowledge_bases
|
||||
|
||||
def search_knowledge_bases(
|
||||
self, user_id: str, filter: dict, skip: int = 0, limit: int = 30
|
||||
self,
|
||||
user_id: str,
|
||||
filter: dict,
|
||||
skip: int = 0,
|
||||
limit: int = 30,
|
||||
db: Optional[Session] = None,
|
||||
) -> KnowledgeListResponse:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Knowledge, User).outerjoin(
|
||||
User, User.id == Knowledge.user_id
|
||||
)
|
||||
|
|
@ -267,17 +273,17 @@ class KnowledgeTable:
|
|||
return KnowledgeListResponse(items=[], total=0)
|
||||
|
||||
def search_knowledge_files(
|
||||
self, filter: dict, skip: int = 0, limit: int = 30
|
||||
self, filter: dict, skip: int = 0, limit: int = 30, db: Optional[Session] = None
|
||||
) -> KnowledgeFileListResponse:
|
||||
"""
|
||||
Scalable version: search files across all knowledge bases the user has
|
||||
READ access to, without loading all KBs or using large IN() lists.
|
||||
"""
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
# Base query: join Knowledge → KnowledgeFile → File
|
||||
query = (
|
||||
db.query(File, User)
|
||||
db.query(File, User, Knowledge)
|
||||
.join(KnowledgeFile, File.id == KnowledgeFile.file_id)
|
||||
.join(Knowledge, KnowledgeFile.knowledge_id == Knowledge.id)
|
||||
.outerjoin(User, User.id == KnowledgeFile.user_id)
|
||||
|
|
@ -307,7 +313,7 @@ class KnowledgeTable:
|
|||
rows = query.all()
|
||||
|
||||
items = []
|
||||
for file, user in rows:
|
||||
for file, user, knowledge in rows:
|
||||
items.append(
|
||||
FileUserResponse(
|
||||
**FileModel.model_validate(file).model_dump(),
|
||||
|
|
@ -318,6 +324,9 @@ class KnowledgeTable:
|
|||
if user
|
||||
else None
|
||||
),
|
||||
collection=KnowledgeModel.model_validate(
|
||||
knowledge
|
||||
).model_dump(),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -327,20 +336,26 @@ class KnowledgeTable:
|
|||
print("search_knowledge_files error:", e)
|
||||
return KnowledgeFileListResponse(items=[], total=0)
|
||||
|
||||
def check_access_by_user_id(self, id, user_id, permission="write") -> bool:
|
||||
knowledge = self.get_knowledge_by_id(id)
|
||||
def check_access_by_user_id(
|
||||
self, id, user_id, permission="write", db: Optional[Session] = None
|
||||
) -> bool:
|
||||
knowledge = self.get_knowledge_by_id(id, db=db)
|
||||
if not knowledge:
|
||||
return False
|
||||
if knowledge.user_id == user_id:
|
||||
return True
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
}
|
||||
return has_access(user_id, permission, knowledge.access_control, user_group_ids)
|
||||
|
||||
def get_knowledge_bases_by_user_id(
|
||||
self, user_id: str, permission: str = "write"
|
||||
self, user_id: str, permission: str = "write", db: Optional[Session] = None
|
||||
) -> list[KnowledgeUserModel]:
|
||||
knowledge_bases = self.get_knowledge_bases()
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
|
||||
knowledge_bases = self.get_knowledge_bases(db=db)
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
}
|
||||
return [
|
||||
knowledge_base
|
||||
for knowledge_base in knowledge_bases
|
||||
|
|
@ -350,32 +365,38 @@ class KnowledgeTable:
|
|||
)
|
||||
]
|
||||
|
||||
def get_knowledge_by_id(self, id: str) -> Optional[KnowledgeModel]:
|
||||
def get_knowledge_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[KnowledgeModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
knowledge = db.query(Knowledge).filter_by(id=id).first()
|
||||
return KnowledgeModel.model_validate(knowledge) if knowledge else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_knowledge_by_id_and_user_id(
|
||||
self, id: str, user_id: str
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[KnowledgeModel]:
|
||||
knowledge = self.get_knowledge_by_id(id)
|
||||
knowledge = self.get_knowledge_by_id(id, db=db)
|
||||
if not knowledge:
|
||||
return None
|
||||
|
||||
if knowledge.user_id == user_id:
|
||||
return knowledge
|
||||
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
}
|
||||
if has_access(user_id, "write", knowledge.access_control, user_group_ids):
|
||||
return knowledge
|
||||
return None
|
||||
|
||||
def get_knowledges_by_file_id(self, file_id: str) -> list[KnowledgeModel]:
|
||||
def get_knowledges_by_file_id(
|
||||
self, file_id: str, db: Optional[Session] = None
|
||||
) -> list[KnowledgeModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
knowledges = (
|
||||
db.query(Knowledge)
|
||||
.join(KnowledgeFile, Knowledge.id == KnowledgeFile.knowledge_id)
|
||||
|
|
@ -395,9 +416,10 @@ class KnowledgeTable:
|
|||
filter: dict,
|
||||
skip: int = 0,
|
||||
limit: int = 30,
|
||||
db: Optional[Session] = None,
|
||||
) -> KnowledgeFileListResponse:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
query = (
|
||||
db.query(File, User)
|
||||
.join(KnowledgeFile, File.id == KnowledgeFile.file_id)
|
||||
|
|
@ -470,9 +492,11 @@ class KnowledgeTable:
|
|||
print(e)
|
||||
return KnowledgeFileListResponse(items=[], total=0)
|
||||
|
||||
def get_files_by_id(self, knowledge_id: str) -> list[FileModel]:
|
||||
def get_files_by_id(
|
||||
self, knowledge_id: str, db: Optional[Session] = None
|
||||
) -> list[FileModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
files = (
|
||||
db.query(File)
|
||||
.join(KnowledgeFile, File.id == KnowledgeFile.file_id)
|
||||
|
|
@ -483,18 +507,24 @@ class KnowledgeTable:
|
|||
except Exception:
|
||||
return []
|
||||
|
||||
def get_file_metadatas_by_id(self, knowledge_id: str) -> list[FileMetadataResponse]:
|
||||
def get_file_metadatas_by_id(
|
||||
self, knowledge_id: str, db: Optional[Session] = None
|
||||
) -> list[FileMetadataResponse]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
files = self.get_files_by_id(knowledge_id)
|
||||
with get_db_context(db) as db:
|
||||
files = self.get_files_by_id(knowledge_id, db=db)
|
||||
return [FileMetadataResponse(**file.model_dump()) for file in files]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def add_file_to_knowledge_by_id(
|
||||
self, knowledge_id: str, file_id: str, user_id: str
|
||||
self,
|
||||
knowledge_id: str,
|
||||
file_id: str,
|
||||
user_id: str,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[KnowledgeFileModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
knowledge_file = KnowledgeFileModel(
|
||||
**{
|
||||
"id": str(uuid.uuid4()),
|
||||
|
|
@ -518,9 +548,11 @@ class KnowledgeTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def remove_file_from_knowledge_by_id(self, knowledge_id: str, file_id: str) -> bool:
|
||||
def remove_file_from_knowledge_by_id(
|
||||
self, knowledge_id: str, file_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(KnowledgeFile).filter_by(
|
||||
knowledge_id=knowledge_id, file_id=file_id
|
||||
).delete()
|
||||
|
|
@ -529,9 +561,11 @@ class KnowledgeTable:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def reset_knowledge_by_id(self, id: str) -> Optional[KnowledgeModel]:
|
||||
def reset_knowledge_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[KnowledgeModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
# Delete all knowledge_file entries for this knowledge_id
|
||||
db.query(KnowledgeFile).filter_by(knowledge_id=id).delete()
|
||||
db.commit()
|
||||
|
|
@ -544,17 +578,21 @@ class KnowledgeTable:
|
|||
)
|
||||
db.commit()
|
||||
|
||||
return self.get_knowledge_by_id(id=id)
|
||||
return self.get_knowledge_by_id(id=id, db=db)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
return None
|
||||
|
||||
def update_knowledge_by_id(
|
||||
self, id: str, form_data: KnowledgeForm, overwrite: bool = False
|
||||
self,
|
||||
id: str,
|
||||
form_data: KnowledgeForm,
|
||||
overwrite: bool = False,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[KnowledgeModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
knowledge = self.get_knowledge_by_id(id=id)
|
||||
with get_db_context(db) as db:
|
||||
knowledge = self.get_knowledge_by_id(id=id, db=db)
|
||||
db.query(Knowledge).filter_by(id=id).update(
|
||||
{
|
||||
**form_data.model_dump(),
|
||||
|
|
@ -562,17 +600,17 @@ class KnowledgeTable:
|
|||
}
|
||||
)
|
||||
db.commit()
|
||||
return self.get_knowledge_by_id(id=id)
|
||||
return self.get_knowledge_by_id(id=id, db=db)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
return None
|
||||
|
||||
def update_knowledge_data_by_id(
|
||||
self, id: str, data: dict
|
||||
self, id: str, data: dict, db: Optional[Session] = None
|
||||
) -> Optional[KnowledgeModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
knowledge = self.get_knowledge_by_id(id=id)
|
||||
with get_db_context(db) as db:
|
||||
knowledge = self.get_knowledge_by_id(id=id, db=db)
|
||||
db.query(Knowledge).filter_by(id=id).update(
|
||||
{
|
||||
"data": data,
|
||||
|
|
@ -580,22 +618,22 @@ class KnowledgeTable:
|
|||
}
|
||||
)
|
||||
db.commit()
|
||||
return self.get_knowledge_by_id(id=id)
|
||||
return self.get_knowledge_by_id(id=id, db=db)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
return None
|
||||
|
||||
def delete_knowledge_by_id(self, id: str) -> bool:
|
||||
def delete_knowledge_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Knowledge).filter_by(id=id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_all_knowledge(self) -> bool:
|
||||
with get_db() as db:
|
||||
def delete_all_knowledge(self, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
db.query(Knowledge).delete()
|
||||
db.commit()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import time
|
|||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.internal.db import Base, get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, get_db, get_db_context
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import BigInteger, Column, String, Text
|
||||
|
||||
|
|
@ -41,8 +42,9 @@ class MemoriesTable:
|
|||
self,
|
||||
user_id: str,
|
||||
content: str,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[MemoryModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
id = str(uuid.uuid4())
|
||||
|
||||
memory = MemoryModel(
|
||||
|
|
@ -68,8 +70,9 @@ class MemoriesTable:
|
|||
id: str,
|
||||
user_id: str,
|
||||
content: str,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[MemoryModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
memory = db.get(Memory, id)
|
||||
if not memory or memory.user_id != user_id:
|
||||
|
|
@ -83,32 +86,36 @@ class MemoriesTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def get_memories(self) -> list[MemoryModel]:
|
||||
with get_db() as db:
|
||||
def get_memories(self, db: Optional[Session] = None) -> list[MemoryModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
memories = db.query(Memory).all()
|
||||
return [MemoryModel.model_validate(memory) for memory in memories]
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_memories_by_user_id(self, user_id: str) -> list[MemoryModel]:
|
||||
with get_db() as db:
|
||||
def get_memories_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> list[MemoryModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
memories = db.query(Memory).filter_by(user_id=user_id).all()
|
||||
return [MemoryModel.model_validate(memory) for memory in memories]
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_memory_by_id(self, id: str) -> Optional[MemoryModel]:
|
||||
with get_db() as db:
|
||||
def get_memory_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[MemoryModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
memory = db.get(Memory, id)
|
||||
return MemoryModel.model_validate(memory)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def delete_memory_by_id(self, id: str) -> bool:
|
||||
with get_db() as db:
|
||||
def delete_memory_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
db.query(Memory).filter_by(id=id).delete()
|
||||
db.commit()
|
||||
|
|
@ -118,8 +125,10 @@ class MemoriesTable:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_memories_by_user_id(self, user_id: str) -> bool:
|
||||
with get_db() as db:
|
||||
def delete_memories_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
db.query(Memory).filter_by(user_id=user_id).delete()
|
||||
db.commit()
|
||||
|
|
@ -128,8 +137,10 @@ class MemoriesTable:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool:
|
||||
with get_db() as db:
|
||||
def delete_memory_by_id_and_user_id(
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
memory = db.get(Memory, id)
|
||||
if not memory or memory.user_id != user_id:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import time
|
|||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.internal.db import Base, get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
from open_webui.models.tags import TagModel, Tag, Tags
|
||||
from open_webui.models.users import Users, User, UserNameResponse
|
||||
from open_webui.models.channels import Channels, ChannelMember
|
||||
|
|
@ -137,9 +138,13 @@ class MessageResponse(MessageReplyToResponse):
|
|||
|
||||
class MessageTable:
|
||||
def insert_new_message(
|
||||
self, form_data: MessageForm, channel_id: str, user_id: str
|
||||
self,
|
||||
form_data: MessageForm,
|
||||
channel_id: str,
|
||||
user_id: str,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[MessageModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
channel_member = Channels.join_channel(channel_id, user_id)
|
||||
|
||||
id = str(uuid.uuid4())
|
||||
|
|
@ -169,26 +174,57 @@ class MessageTable:
|
|||
db.refresh(result)
|
||||
return MessageModel.model_validate(result) if result else None
|
||||
|
||||
def get_message_by_id(self, id: str) -> Optional[MessageResponse]:
|
||||
with get_db() as db:
|
||||
def get_message_by_id(
|
||||
self,
|
||||
id: str,
|
||||
include_thread_replies: Optional[bool] = True,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[MessageResponse]:
|
||||
with get_db_context(db) as db:
|
||||
message = db.get(Message, id)
|
||||
if not message:
|
||||
return None
|
||||
|
||||
reply_to_message = (
|
||||
self.get_message_by_id(message.reply_to_id)
|
||||
self.get_message_by_id(
|
||||
message.reply_to_id, include_thread_replies=False, db=db
|
||||
)
|
||||
if message.reply_to_id
|
||||
else None
|
||||
)
|
||||
|
||||
reactions = self.get_reactions_by_message_id(id)
|
||||
thread_replies = self.get_thread_replies_by_message_id(id)
|
||||
reactions = self.get_reactions_by_message_id(id, db=db)
|
||||
|
||||
thread_replies = []
|
||||
if include_thread_replies:
|
||||
thread_replies = self.get_thread_replies_by_message_id(id, db=db)
|
||||
|
||||
# Check if message was sent by webhook (webhook info in meta takes precedence)
|
||||
webhook_info = message.meta.get("webhook") if message.meta else None
|
||||
if webhook_info and webhook_info.get("id"):
|
||||
# Look up webhook by ID to get current name
|
||||
webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db)
|
||||
if webhook:
|
||||
user_info = {
|
||||
"id": webhook.id,
|
||||
"name": webhook.name,
|
||||
"role": "webhook",
|
||||
}
|
||||
else:
|
||||
# Webhook was deleted, use placeholder
|
||||
user_info = {
|
||||
"id": webhook_info.get("id"),
|
||||
"name": "Deleted Webhook",
|
||||
"role": "webhook",
|
||||
}
|
||||
else:
|
||||
user = Users.get_user_by_id(message.user_id, db=db)
|
||||
user_info = user.model_dump() if user else None
|
||||
|
||||
user = Users.get_user_by_id(message.user_id)
|
||||
return MessageResponse.model_validate(
|
||||
{
|
||||
**MessageModel.model_validate(message).model_dump(),
|
||||
"user": user.model_dump() if user else None,
|
||||
"user": user_info,
|
||||
"reply_to_message": (
|
||||
reply_to_message.model_dump() if reply_to_message else None
|
||||
),
|
||||
|
|
@ -200,8 +236,10 @@ class MessageTable:
|
|||
}
|
||||
)
|
||||
|
||||
def get_thread_replies_by_message_id(self, id: str) -> list[MessageReplyToResponse]:
|
||||
with get_db() as db:
|
||||
def get_thread_replies_by_message_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> list[MessageReplyToResponse]:
|
||||
with get_db_context(db) as db:
|
||||
all_messages = (
|
||||
db.query(Message)
|
||||
.filter_by(parent_id=id)
|
||||
|
|
@ -212,14 +250,35 @@ class MessageTable:
|
|||
messages = []
|
||||
for message in all_messages:
|
||||
reply_to_message = (
|
||||
self.get_message_by_id(message.reply_to_id)
|
||||
self.get_message_by_id(
|
||||
message.reply_to_id, include_thread_replies=False, db=db
|
||||
)
|
||||
if message.reply_to_id
|
||||
else None
|
||||
)
|
||||
|
||||
webhook_info = message.meta.get("webhook") if message.meta else None
|
||||
user_info = None
|
||||
if webhook_info and webhook_info.get("id"):
|
||||
webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db)
|
||||
if webhook:
|
||||
user_info = {
|
||||
"id": webhook.id,
|
||||
"name": webhook.name,
|
||||
"role": "webhook",
|
||||
}
|
||||
else:
|
||||
user_info = {
|
||||
"id": webhook_info.get("id"),
|
||||
"name": "Deleted Webhook",
|
||||
"role": "webhook",
|
||||
}
|
||||
|
||||
messages.append(
|
||||
MessageReplyToResponse.model_validate(
|
||||
{
|
||||
**MessageModel.model_validate(message).model_dump(),
|
||||
"user": user_info,
|
||||
"reply_to_message": (
|
||||
reply_to_message.model_dump()
|
||||
if reply_to_message
|
||||
|
|
@ -230,17 +289,23 @@ class MessageTable:
|
|||
)
|
||||
return messages
|
||||
|
||||
def get_reply_user_ids_by_message_id(self, id: str) -> list[str]:
|
||||
with get_db() as db:
|
||||
def get_reply_user_ids_by_message_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> list[str]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
message.user_id
|
||||
for message in db.query(Message).filter_by(parent_id=id).all()
|
||||
]
|
||||
|
||||
def get_messages_by_channel_id(
|
||||
self, channel_id: str, skip: int = 0, limit: int = 50
|
||||
self,
|
||||
channel_id: str,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: Optional[Session] = None,
|
||||
) -> list[MessageReplyToResponse]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
all_messages = (
|
||||
db.query(Message)
|
||||
.filter_by(channel_id=channel_id, parent_id=None)
|
||||
|
|
@ -253,14 +318,35 @@ class MessageTable:
|
|||
messages = []
|
||||
for message in all_messages:
|
||||
reply_to_message = (
|
||||
self.get_message_by_id(message.reply_to_id)
|
||||
self.get_message_by_id(
|
||||
message.reply_to_id, include_thread_replies=False, db=db
|
||||
)
|
||||
if message.reply_to_id
|
||||
else None
|
||||
)
|
||||
|
||||
webhook_info = message.meta.get("webhook") if message.meta else None
|
||||
user_info = None
|
||||
if webhook_info and webhook_info.get("id"):
|
||||
webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db)
|
||||
if webhook:
|
||||
user_info = {
|
||||
"id": webhook.id,
|
||||
"name": webhook.name,
|
||||
"role": "webhook",
|
||||
}
|
||||
else:
|
||||
user_info = {
|
||||
"id": webhook_info.get("id"),
|
||||
"name": "Deleted Webhook",
|
||||
"role": "webhook",
|
||||
}
|
||||
|
||||
messages.append(
|
||||
MessageReplyToResponse.model_validate(
|
||||
{
|
||||
**MessageModel.model_validate(message).model_dump(),
|
||||
"user": user_info,
|
||||
"reply_to_message": (
|
||||
reply_to_message.model_dump()
|
||||
if reply_to_message
|
||||
|
|
@ -272,9 +358,14 @@ class MessageTable:
|
|||
return messages
|
||||
|
||||
def get_messages_by_parent_id(
|
||||
self, channel_id: str, parent_id: str, skip: int = 0, limit: int = 50
|
||||
self,
|
||||
channel_id: str,
|
||||
parent_id: str,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: Optional[Session] = None,
|
||||
) -> list[MessageReplyToResponse]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
message = db.get(Message, parent_id)
|
||||
|
||||
if not message:
|
||||
|
|
@ -296,14 +387,35 @@ class MessageTable:
|
|||
messages = []
|
||||
for message in all_messages:
|
||||
reply_to_message = (
|
||||
self.get_message_by_id(message.reply_to_id)
|
||||
self.get_message_by_id(
|
||||
message.reply_to_id, include_thread_replies=False, db=db
|
||||
)
|
||||
if message.reply_to_id
|
||||
else None
|
||||
)
|
||||
|
||||
webhook_info = message.meta.get("webhook") if message.meta else None
|
||||
user_info = None
|
||||
if webhook_info and webhook_info.get("id"):
|
||||
webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db)
|
||||
if webhook:
|
||||
user_info = {
|
||||
"id": webhook.id,
|
||||
"name": webhook.name,
|
||||
"role": "webhook",
|
||||
}
|
||||
else:
|
||||
user_info = {
|
||||
"id": webhook_info.get("id"),
|
||||
"name": "Deleted Webhook",
|
||||
"role": "webhook",
|
||||
}
|
||||
|
||||
messages.append(
|
||||
MessageReplyToResponse.model_validate(
|
||||
{
|
||||
**MessageModel.model_validate(message).model_dump(),
|
||||
"user": user_info,
|
||||
"reply_to_message": (
|
||||
reply_to_message.model_dump()
|
||||
if reply_to_message
|
||||
|
|
@ -314,8 +426,10 @@ class MessageTable:
|
|||
)
|
||||
return messages
|
||||
|
||||
def get_last_message_by_channel_id(self, channel_id: str) -> Optional[MessageModel]:
|
||||
with get_db() as db:
|
||||
def get_last_message_by_channel_id(
|
||||
self, channel_id: str, db: Optional[Session] = None
|
||||
) -> Optional[MessageModel]:
|
||||
with get_db_context(db) as db:
|
||||
message = (
|
||||
db.query(Message)
|
||||
.filter_by(channel_id=channel_id)
|
||||
|
|
@ -325,9 +439,13 @@ class MessageTable:
|
|||
return MessageModel.model_validate(message) if message else None
|
||||
|
||||
def get_pinned_messages_by_channel_id(
|
||||
self, channel_id: str, skip: int = 0, limit: int = 50
|
||||
self,
|
||||
channel_id: str,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: Optional[Session] = None,
|
||||
) -> list[MessageModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
all_messages = (
|
||||
db.query(Message)
|
||||
.filter_by(channel_id=channel_id, is_pinned=True)
|
||||
|
|
@ -339,9 +457,9 @@ class MessageTable:
|
|||
return [MessageModel.model_validate(message) for message in all_messages]
|
||||
|
||||
def update_message_by_id(
|
||||
self, id: str, form_data: MessageForm
|
||||
self, id: str, form_data: MessageForm, db: Optional[Session] = None
|
||||
) -> Optional[MessageModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
message = db.get(Message, id)
|
||||
message.content = form_data.content
|
||||
message.data = {
|
||||
|
|
@ -358,9 +476,13 @@ class MessageTable:
|
|||
return MessageModel.model_validate(message) if message else None
|
||||
|
||||
def update_is_pinned_by_id(
|
||||
self, id: str, is_pinned: bool, pinned_by: Optional[str] = None
|
||||
self,
|
||||
id: str,
|
||||
is_pinned: bool,
|
||||
pinned_by: Optional[str] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[MessageModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
message = db.get(Message, id)
|
||||
message.is_pinned = is_pinned
|
||||
message.pinned_at = int(time.time_ns()) if is_pinned else None
|
||||
|
|
@ -370,9 +492,13 @@ class MessageTable:
|
|||
return MessageModel.model_validate(message) if message else None
|
||||
|
||||
def get_unread_message_count(
|
||||
self, channel_id: str, user_id: str, last_read_at: Optional[int] = None
|
||||
self,
|
||||
channel_id: str,
|
||||
user_id: str,
|
||||
last_read_at: Optional[int] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> int:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Message).filter(
|
||||
Message.channel_id == channel_id,
|
||||
Message.parent_id == None, # only count top-level messages
|
||||
|
|
@ -383,9 +509,9 @@ class MessageTable:
|
|||
return query.count()
|
||||
|
||||
def add_reaction_to_message(
|
||||
self, id: str, user_id: str, name: str
|
||||
self, id: str, user_id: str, name: str, db: Optional[Session] = None
|
||||
) -> Optional[MessageReactionModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
# check for existing reaction
|
||||
existing_reaction = (
|
||||
db.query(MessageReaction)
|
||||
|
|
@ -409,8 +535,10 @@ class MessageTable:
|
|||
db.refresh(result)
|
||||
return MessageReactionModel.model_validate(result) if result else None
|
||||
|
||||
def get_reactions_by_message_id(self, id: str) -> list[Reactions]:
|
||||
with get_db() as db:
|
||||
def get_reactions_by_message_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> list[Reactions]:
|
||||
with get_db_context(db) as db:
|
||||
# JOIN User so all user info is fetched in one query
|
||||
results = (
|
||||
db.query(MessageReaction, User)
|
||||
|
|
@ -440,29 +568,29 @@ class MessageTable:
|
|||
return [Reactions(**reaction) for reaction in reactions.values()]
|
||||
|
||||
def remove_reaction_by_id_and_user_id_and_name(
|
||||
self, id: str, user_id: str, name: str
|
||||
self, id: str, user_id: str, name: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(MessageReaction).filter_by(
|
||||
message_id=id, user_id=user_id, name=name
|
||||
).delete()
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
def delete_reactions_by_id(self, id: str) -> bool:
|
||||
with get_db() as db:
|
||||
def delete_reactions_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
db.query(MessageReaction).filter_by(message_id=id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
def delete_replies_by_id(self, id: str) -> bool:
|
||||
with get_db() as db:
|
||||
def delete_replies_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Message).filter_by(parent_id=id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
def delete_message_by_id(self, id: str) -> bool:
|
||||
with get_db() as db:
|
||||
def delete_message_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Message).filter_by(id=id).delete()
|
||||
|
||||
# Delete all reactions to this message
|
||||
|
|
@ -471,5 +599,35 @@ class MessageTable:
|
|||
db.commit()
|
||||
return True
|
||||
|
||||
def search_messages_by_channel_ids(
|
||||
self,
|
||||
channel_ids: list[str],
|
||||
query: str,
|
||||
start_timestamp: Optional[int] = None,
|
||||
end_timestamp: Optional[int] = None,
|
||||
limit: int = 10,
|
||||
db: Optional[Session] = None,
|
||||
) -> list[MessageModel]:
|
||||
"""Search messages in specified channels by content."""
|
||||
with get_db_context(db) as db:
|
||||
query_builder = db.query(Message).filter(
|
||||
Message.channel_id.in_(channel_ids),
|
||||
Message.content.ilike(f"%{query}%"),
|
||||
)
|
||||
|
||||
if start_timestamp:
|
||||
query_builder = query_builder.filter(
|
||||
Message.created_at >= start_timestamp
|
||||
)
|
||||
if end_timestamp:
|
||||
query_builder = query_builder.filter(
|
||||
Message.created_at <= end_timestamp
|
||||
)
|
||||
|
||||
messages = (
|
||||
query_builder.order_by(Message.created_at.desc()).limit(limit).all()
|
||||
)
|
||||
return [MessageModel.model_validate(msg) for msg in messages]
|
||||
|
||||
|
||||
Messages = MessageTable()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import logging
|
|||
import time
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.internal.db import Base, JSONField, get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
|
||||
from open_webui.models.groups import Groups
|
||||
from open_webui.models.users import User, UserModel, Users, UserResponse
|
||||
|
|
@ -129,6 +130,10 @@ class ModelUserResponse(ModelModel):
|
|||
user: Optional[UserResponse] = None
|
||||
|
||||
|
||||
class ModelAccessResponse(ModelUserResponse):
|
||||
write_access: Optional[bool] = False
|
||||
|
||||
|
||||
class ModelResponse(ModelModel):
|
||||
pass
|
||||
|
||||
|
|
@ -138,6 +143,11 @@ class ModelListResponse(BaseModel):
|
|||
total: int
|
||||
|
||||
|
||||
class ModelAccessListResponse(BaseModel):
|
||||
items: list[ModelAccessResponse]
|
||||
total: int
|
||||
|
||||
|
||||
class ModelForm(BaseModel):
|
||||
id: str
|
||||
base_model_id: Optional[str] = None
|
||||
|
|
@ -150,7 +160,7 @@ class ModelForm(BaseModel):
|
|||
|
||||
class ModelsTable:
|
||||
def insert_new_model(
|
||||
self, form_data: ModelForm, user_id: str
|
||||
self, form_data: ModelForm, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[ModelModel]:
|
||||
model = ModelModel(
|
||||
**{
|
||||
|
|
@ -161,7 +171,7 @@ class ModelsTable:
|
|||
}
|
||||
)
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
result = Model(**model.model_dump())
|
||||
db.add(result)
|
||||
db.commit()
|
||||
|
|
@ -175,17 +185,17 @@ class ModelsTable:
|
|||
log.exception(f"Failed to insert a new model: {e}")
|
||||
return None
|
||||
|
||||
def get_all_models(self) -> list[ModelModel]:
|
||||
with get_db() as db:
|
||||
def get_all_models(self, db: Optional[Session] = None) -> list[ModelModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [ModelModel.model_validate(model) for model in db.query(Model).all()]
|
||||
|
||||
def get_models(self) -> list[ModelUserResponse]:
|
||||
with get_db() as db:
|
||||
def get_models(self, db: Optional[Session] = None) -> list[ModelUserResponse]:
|
||||
with get_db_context(db) as db:
|
||||
all_models = db.query(Model).filter(Model.base_model_id != None).all()
|
||||
|
||||
user_ids = list(set(model.user_id for model in all_models))
|
||||
|
||||
users = Users.get_users_by_user_ids(user_ids) if user_ids else []
|
||||
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
|
||||
users_dict = {user.id: user for user in users}
|
||||
|
||||
models = []
|
||||
|
|
@ -201,18 +211,20 @@ class ModelsTable:
|
|||
)
|
||||
return models
|
||||
|
||||
def get_base_models(self) -> list[ModelModel]:
|
||||
with get_db() as db:
|
||||
def get_base_models(self, db: Optional[Session] = None) -> list[ModelModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
ModelModel.model_validate(model)
|
||||
for model in db.query(Model).filter(Model.base_model_id == None).all()
|
||||
]
|
||||
|
||||
def get_models_by_user_id(
|
||||
self, user_id: str, permission: str = "write"
|
||||
self, user_id: str, permission: str = "write", db: Optional[Session] = None
|
||||
) -> list[ModelUserResponse]:
|
||||
models = self.get_models()
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
|
||||
models = self.get_models(db=db)
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
}
|
||||
return [
|
||||
model
|
||||
for model in models
|
||||
|
|
@ -263,9 +275,14 @@ class ModelsTable:
|
|||
return query
|
||||
|
||||
def search_models(
|
||||
self, user_id: str, filter: dict = {}, skip: int = 0, limit: int = 30
|
||||
self,
|
||||
user_id: str,
|
||||
filter: dict = {},
|
||||
skip: int = 0,
|
||||
limit: int = 30,
|
||||
db: Optional[Session] = None,
|
||||
) -> ModelListResponse:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
# Join GroupMember so we can order by group_id when requested
|
||||
query = db.query(Model, User).outerjoin(User, User.id == Model.user_id)
|
||||
query = query.filter(Model.base_model_id != None)
|
||||
|
|
@ -291,7 +308,7 @@ class ModelsTable:
|
|||
db,
|
||||
query,
|
||||
filter,
|
||||
permission="write",
|
||||
permission="read",
|
||||
)
|
||||
|
||||
tag = filter.get("tag")
|
||||
|
|
@ -349,24 +366,30 @@ class ModelsTable:
|
|||
|
||||
return ModelListResponse(items=models, total=total)
|
||||
|
||||
def get_model_by_id(self, id: str) -> Optional[ModelModel]:
|
||||
def get_model_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[ModelModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
model = db.get(Model, id)
|
||||
return ModelModel.model_validate(model)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_models_by_ids(self, ids: list[str]) -> list[ModelModel]:
|
||||
def get_models_by_ids(
|
||||
self, ids: list[str], db: Optional[Session] = None
|
||||
) -> list[ModelModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
models = db.query(Model).filter(Model.id.in_(ids)).all()
|
||||
return [ModelModel.model_validate(model) for model in models]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def toggle_model_by_id(self, id: str) -> Optional[ModelModel]:
|
||||
with get_db() as db:
|
||||
def toggle_model_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[ModelModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
is_active = db.query(Model).filter_by(id=id).first().is_active
|
||||
|
||||
|
|
@ -378,13 +401,15 @@ class ModelsTable:
|
|||
)
|
||||
db.commit()
|
||||
|
||||
return self.get_model_by_id(id)
|
||||
return self.get_model_by_id(id, db=db)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
|
||||
def update_model_by_id(
|
||||
self, id: str, model: ModelForm, db: Optional[Session] = None
|
||||
) -> Optional[ModelModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
# update only the fields that are present in the model
|
||||
data = model.model_dump(exclude={"id"})
|
||||
result = db.query(Model).filter_by(id=id).update(data)
|
||||
|
|
@ -398,9 +423,9 @@ class ModelsTable:
|
|||
log.exception(f"Failed to update the model by id {id}: {e}")
|
||||
return None
|
||||
|
||||
def delete_model_by_id(self, id: str) -> bool:
|
||||
def delete_model_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Model).filter_by(id=id).delete()
|
||||
db.commit()
|
||||
|
||||
|
|
@ -408,9 +433,9 @@ class ModelsTable:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_all_models(self) -> bool:
|
||||
def delete_all_models(self, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Model).delete()
|
||||
db.commit()
|
||||
|
||||
|
|
@ -418,9 +443,11 @@ class ModelsTable:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def sync_models(self, user_id: str, models: list[ModelModel]) -> list[ModelModel]:
|
||||
def sync_models(
|
||||
self, user_id: str, models: list[ModelModel], db: Optional[Session] = None
|
||||
) -> list[ModelModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
# Get existing models
|
||||
existing_models = db.query(Model).all()
|
||||
existing_ids = {model.id for model in existing_models}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import uuid
|
|||
from typing import Optional
|
||||
from functools import lru_cache
|
||||
|
||||
from open_webui.internal.db import Base, get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, get_db, get_db_context
|
||||
from open_webui.models.groups import Groups
|
||||
from open_webui.utils.access_control import has_access
|
||||
from open_webui.models.users import User, UserModel, Users, UserResponse
|
||||
|
|
@ -211,11 +212,9 @@ class NoteTable:
|
|||
return query
|
||||
|
||||
def insert_new_note(
|
||||
self,
|
||||
form_data: NoteForm,
|
||||
user_id: str,
|
||||
self, user_id: str, form_data: NoteForm, db: Optional[Session] = None
|
||||
) -> Optional[NoteModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
note = NoteModel(
|
||||
**{
|
||||
"id": str(uuid.uuid4()),
|
||||
|
|
@ -233,9 +232,9 @@ class NoteTable:
|
|||
return note
|
||||
|
||||
def get_notes(
|
||||
self, skip: Optional[int] = None, limit: Optional[int] = None
|
||||
self, skip: int = 0, limit: int = 50, db: Optional[Session] = None
|
||||
) -> list[NoteModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Note).order_by(Note.updated_at.desc())
|
||||
if skip is not None:
|
||||
query = query.offset(skip)
|
||||
|
|
@ -245,19 +244,32 @@ class NoteTable:
|
|||
return [NoteModel.model_validate(note) for note in notes]
|
||||
|
||||
def search_notes(
|
||||
self, user_id: str, filter: dict = {}, skip: int = 0, limit: int = 30
|
||||
self,
|
||||
user_id: str,
|
||||
filter: dict = {},
|
||||
skip: int = 0,
|
||||
limit: int = 30,
|
||||
db: Optional[Session] = None,
|
||||
) -> NoteListResponse:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Note, User).outerjoin(User, User.id == Note.user_id)
|
||||
if filter:
|
||||
query_key = filter.get("query")
|
||||
if query_key:
|
||||
# Normalize search by removing hyphens and spaces (e.g., "todo" matches "to-do" and "to do")
|
||||
normalized_query = query_key.replace("-", "").replace(" ", "")
|
||||
query = query.filter(
|
||||
or_(
|
||||
Note.title.ilike(f"%{query_key}%"),
|
||||
cast(Note.data["content"]["md"], Text).ilike(
|
||||
f"%{query_key}%"
|
||||
),
|
||||
func.replace(
|
||||
func.replace(Note.title, "-", ""), " ", ""
|
||||
).ilike(f"%{normalized_query}%"),
|
||||
func.replace(
|
||||
func.replace(
|
||||
cast(Note.data["content"]["md"], Text), "-", ""
|
||||
),
|
||||
" ",
|
||||
"",
|
||||
).ilike(f"%{normalized_query}%"),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -333,12 +345,13 @@ class NoteTable:
|
|||
self,
|
||||
user_id: str,
|
||||
permission: str = "read",
|
||||
skip: Optional[int] = None,
|
||||
limit: Optional[int] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: Optional[Session] = None,
|
||||
) -> list[NoteModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
user_group_ids = [
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id)
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
]
|
||||
|
||||
query = db.query(Note).order_by(Note.updated_at.desc())
|
||||
|
|
@ -354,15 +367,17 @@ class NoteTable:
|
|||
notes = query.all()
|
||||
return [NoteModel.model_validate(note) for note in notes]
|
||||
|
||||
def get_note_by_id(self, id: str) -> Optional[NoteModel]:
|
||||
with get_db() as db:
|
||||
def get_note_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[NoteModel]:
|
||||
with get_db_context(db) as db:
|
||||
note = db.query(Note).filter(Note.id == id).first()
|
||||
return NoteModel.model_validate(note) if note else None
|
||||
|
||||
def update_note_by_id(
|
||||
self, id: str, form_data: NoteUpdateForm
|
||||
self, id: str, form_data: NoteUpdateForm, db: Optional[Session] = None
|
||||
) -> Optional[NoteModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
note = db.query(Note).filter(Note.id == id).first()
|
||||
if not note:
|
||||
return None
|
||||
|
|
@ -384,11 +399,14 @@ class NoteTable:
|
|||
db.commit()
|
||||
return NoteModel.model_validate(note) if note else None
|
||||
|
||||
def delete_note_by_id(self, id: str):
|
||||
with get_db() as db:
|
||||
db.query(Note).filter(Note.id == id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
def delete_note_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Note).filter(Note.id == id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
Notes = NoteTable()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import json
|
|||
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
from open_webui.internal.db import Base, get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, get_db, get_db_context
|
||||
from open_webui.env import OAUTH_SESSION_TOKEN_ENCRYPTION_KEY
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
|
@ -109,10 +110,11 @@ class OAuthSessionTable:
|
|||
user_id: str,
|
||||
provider: str,
|
||||
token: dict,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[OAuthSessionModel]:
|
||||
"""Create a new OAuth session"""
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
current_time = int(time.time())
|
||||
id = str(uuid.uuid4())
|
||||
|
||||
|
|
@ -141,10 +143,12 @@ class OAuthSessionTable:
|
|||
log.error(f"Error creating OAuth session: {e}")
|
||||
return None
|
||||
|
||||
def get_session_by_id(self, session_id: str) -> Optional[OAuthSessionModel]:
|
||||
def get_session_by_id(
|
||||
self, session_id: str, db: Optional[Session] = None
|
||||
) -> Optional[OAuthSessionModel]:
|
||||
"""Get OAuth session by ID"""
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
session = db.query(OAuthSession).filter_by(id=session_id).first()
|
||||
if session:
|
||||
session.token = self._decrypt_token(session.token)
|
||||
|
|
@ -156,11 +160,11 @@ class OAuthSessionTable:
|
|||
return None
|
||||
|
||||
def get_session_by_id_and_user_id(
|
||||
self, session_id: str, user_id: str
|
||||
self, session_id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[OAuthSessionModel]:
|
||||
"""Get OAuth session by ID and user ID"""
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
session = (
|
||||
db.query(OAuthSession)
|
||||
.filter_by(id=session_id, user_id=user_id)
|
||||
|
|
@ -176,11 +180,11 @@ class OAuthSessionTable:
|
|||
return None
|
||||
|
||||
def get_session_by_provider_and_user_id(
|
||||
self, provider: str, user_id: str
|
||||
self, provider: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[OAuthSessionModel]:
|
||||
"""Get OAuth session by provider and user ID"""
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
session = (
|
||||
db.query(OAuthSession)
|
||||
.filter_by(provider=provider, user_id=user_id)
|
||||
|
|
@ -195,10 +199,12 @@ class OAuthSessionTable:
|
|||
log.error(f"Error getting OAuth session by provider and user ID: {e}")
|
||||
return None
|
||||
|
||||
def get_sessions_by_user_id(self, user_id: str) -> List[OAuthSessionModel]:
|
||||
def get_sessions_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> List[OAuthSessionModel]:
|
||||
"""Get all OAuth sessions for a user"""
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
sessions = db.query(OAuthSession).filter_by(user_id=user_id).all()
|
||||
|
||||
results = []
|
||||
|
|
@ -213,11 +219,11 @@ class OAuthSessionTable:
|
|||
return []
|
||||
|
||||
def update_session_by_id(
|
||||
self, session_id: str, token: dict
|
||||
self, session_id: str, token: dict, db: Optional[Session] = None
|
||||
) -> Optional[OAuthSessionModel]:
|
||||
"""Update OAuth session tokens"""
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
current_time = int(time.time())
|
||||
|
||||
db.query(OAuthSession).filter_by(id=session_id).update(
|
||||
|
|
@ -239,10 +245,12 @@ class OAuthSessionTable:
|
|||
log.error(f"Error updating OAuth session tokens: {e}")
|
||||
return None
|
||||
|
||||
def delete_session_by_id(self, session_id: str) -> bool:
|
||||
def delete_session_by_id(
|
||||
self, session_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
"""Delete an OAuth session"""
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
result = db.query(OAuthSession).filter_by(id=session_id).delete()
|
||||
db.commit()
|
||||
return result > 0
|
||||
|
|
@ -250,10 +258,12 @@ class OAuthSessionTable:
|
|||
log.error(f"Error deleting OAuth session: {e}")
|
||||
return False
|
||||
|
||||
def delete_sessions_by_user_id(self, user_id: str) -> bool:
|
||||
def delete_sessions_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
"""Delete all OAuth sessions for a user"""
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
result = db.query(OAuthSession).filter_by(user_id=user_id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
|
|
@ -261,10 +271,12 @@ class OAuthSessionTable:
|
|||
log.error(f"Error deleting OAuth sessions by user ID: {e}")
|
||||
return False
|
||||
|
||||
def delete_sessions_by_provider(self, provider: str) -> bool:
|
||||
def delete_sessions_by_provider(
|
||||
self, provider: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
"""Delete all OAuth sessions for a provider"""
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(OAuthSession).filter_by(provider=provider).delete()
|
||||
db.commit()
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import time
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.internal.db import Base, get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
from open_webui.models.groups import Groups
|
||||
from open_webui.models.users import Users, UserResponse
|
||||
|
||||
|
|
@ -62,6 +63,10 @@ class PromptUserResponse(PromptModel):
|
|||
user: Optional[UserResponse] = None
|
||||
|
||||
|
||||
class PromptAccessResponse(PromptUserResponse):
|
||||
write_access: Optional[bool] = False
|
||||
|
||||
|
||||
class PromptForm(BaseModel):
|
||||
command: str
|
||||
title: str
|
||||
|
|
@ -71,7 +76,7 @@ class PromptForm(BaseModel):
|
|||
|
||||
class PromptsTable:
|
||||
def insert_new_prompt(
|
||||
self, user_id: str, form_data: PromptForm
|
||||
self, user_id: str, form_data: PromptForm, db: Optional[Session] = None
|
||||
) -> Optional[PromptModel]:
|
||||
prompt = PromptModel(
|
||||
**{
|
||||
|
|
@ -82,7 +87,7 @@ class PromptsTable:
|
|||
)
|
||||
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
result = Prompt(**prompt.model_dump())
|
||||
db.add(result)
|
||||
db.commit()
|
||||
|
|
@ -94,21 +99,23 @@ class PromptsTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def get_prompt_by_command(self, command: str) -> Optional[PromptModel]:
|
||||
def get_prompt_by_command(
|
||||
self, command: str, db: Optional[Session] = None
|
||||
) -> Optional[PromptModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
prompt = db.query(Prompt).filter_by(command=command).first()
|
||||
return PromptModel.model_validate(prompt)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_prompts(self) -> list[PromptUserResponse]:
|
||||
with get_db() as db:
|
||||
def get_prompts(self, db: Optional[Session] = None) -> list[PromptUserResponse]:
|
||||
with get_db_context(db) as db:
|
||||
all_prompts = db.query(Prompt).order_by(Prompt.timestamp.desc()).all()
|
||||
|
||||
user_ids = list(set(prompt.user_id for prompt in all_prompts))
|
||||
|
||||
users = Users.get_users_by_user_ids(user_ids) if user_ids else []
|
||||
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
|
||||
users_dict = {user.id: user for user in users}
|
||||
|
||||
prompts = []
|
||||
|
|
@ -126,10 +133,12 @@ class PromptsTable:
|
|||
return prompts
|
||||
|
||||
def get_prompts_by_user_id(
|
||||
self, user_id: str, permission: str = "write"
|
||||
self, user_id: str, permission: str = "write", db: Optional[Session] = None
|
||||
) -> list[PromptUserResponse]:
|
||||
prompts = self.get_prompts()
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
|
||||
prompts = self.get_prompts(db=db)
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
}
|
||||
|
||||
return [
|
||||
prompt
|
||||
|
|
@ -139,10 +148,10 @@ class PromptsTable:
|
|||
]
|
||||
|
||||
def update_prompt_by_command(
|
||||
self, command: str, form_data: PromptForm
|
||||
self, command: str, form_data: PromptForm, db: Optional[Session] = None
|
||||
) -> Optional[PromptModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
prompt = db.query(Prompt).filter_by(command=command).first()
|
||||
prompt.title = form_data.title
|
||||
prompt.content = form_data.content
|
||||
|
|
@ -153,9 +162,11 @@ class PromptsTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def delete_prompt_by_command(self, command: str) -> bool:
|
||||
def delete_prompt_by_command(
|
||||
self, command: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Prompt).filter_by(command=command).delete()
|
||||
db.commit()
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import time
|
|||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.internal.db import Base, get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
|
@ -50,8 +51,10 @@ class TagChatIdForm(BaseModel):
|
|||
|
||||
|
||||
class TagTable:
|
||||
def insert_new_tag(self, name: str, user_id: str) -> Optional[TagModel]:
|
||||
with get_db() as db:
|
||||
def insert_new_tag(
|
||||
self, name: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[TagModel]:
|
||||
with get_db_context(db) as db:
|
||||
id = name.replace(" ", "_").lower()
|
||||
tag = TagModel(**{"id": id, "user_id": user_id, "name": name})
|
||||
try:
|
||||
|
|
@ -68,27 +71,29 @@ class TagTable:
|
|||
return None
|
||||
|
||||
def get_tag_by_name_and_user_id(
|
||||
self, name: str, user_id: str
|
||||
self, name: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[TagModel]:
|
||||
try:
|
||||
id = name.replace(" ", "_").lower()
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
tag = db.query(Tag).filter_by(id=id, user_id=user_id).first()
|
||||
return TagModel.model_validate(tag)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_tags_by_user_id(self, user_id: str) -> list[TagModel]:
|
||||
with get_db() as db:
|
||||
def get_tags_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> list[TagModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
TagModel.model_validate(tag)
|
||||
for tag in (db.query(Tag).filter_by(user_id=user_id).all())
|
||||
]
|
||||
|
||||
def get_tags_by_ids_and_user_id(
|
||||
self, ids: list[str], user_id: str
|
||||
self, ids: list[str], user_id: str, db: Optional[Session] = None
|
||||
) -> list[TagModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
TagModel.model_validate(tag)
|
||||
for tag in (
|
||||
|
|
@ -96,9 +101,11 @@ class TagTable:
|
|||
)
|
||||
]
|
||||
|
||||
def delete_tag_by_name_and_user_id(self, name: str, user_id: str) -> bool:
|
||||
def delete_tag_by_name_and_user_id(
|
||||
self, name: str, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
id = name.replace(" ", "_").lower()
|
||||
res = db.query(Tag).filter_by(id=id, user_id=user_id).delete()
|
||||
log.debug(f"res: {res}")
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import logging
|
|||
import time
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.internal.db import Base, JSONField, get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
from open_webui.models.users import Users, UserResponse
|
||||
from open_webui.models.groups import Groups
|
||||
|
||||
|
|
@ -96,6 +97,10 @@ class ToolUserResponse(ToolResponse):
|
|||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
class ToolAccessResponse(ToolUserResponse):
|
||||
write_access: Optional[bool] = False
|
||||
|
||||
|
||||
class ToolForm(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
|
|
@ -110,9 +115,13 @@ class ToolValves(BaseModel):
|
|||
|
||||
class ToolsTable:
|
||||
def insert_new_tool(
|
||||
self, user_id: str, form_data: ToolForm, specs: list[dict]
|
||||
self,
|
||||
user_id: str,
|
||||
form_data: ToolForm,
|
||||
specs: list[dict],
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[ToolModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
tool = ToolModel(
|
||||
**{
|
||||
**form_data.model_dump(),
|
||||
|
|
@ -136,21 +145,23 @@ class ToolsTable:
|
|||
log.exception(f"Error creating a new tool: {e}")
|
||||
return None
|
||||
|
||||
def get_tool_by_id(self, id: str) -> Optional[ToolModel]:
|
||||
def get_tool_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[ToolModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
tool = db.get(Tool, id)
|
||||
return ToolModel.model_validate(tool)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_tools(self) -> list[ToolUserModel]:
|
||||
with get_db() as db:
|
||||
def get_tools(self, db: Optional[Session] = None) -> list[ToolUserModel]:
|
||||
with get_db_context(db) as db:
|
||||
all_tools = db.query(Tool).order_by(Tool.updated_at.desc()).all()
|
||||
|
||||
user_ids = list(set(tool.user_id for tool in all_tools))
|
||||
|
||||
users = Users.get_users_by_user_ids(user_ids) if user_ids else []
|
||||
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
|
||||
users_dict = {user.id: user for user in users}
|
||||
|
||||
tools = []
|
||||
|
|
@ -167,10 +178,12 @@ class ToolsTable:
|
|||
return tools
|
||||
|
||||
def get_tools_by_user_id(
|
||||
self, user_id: str, permission: str = "write"
|
||||
self, user_id: str, permission: str = "write", db: Optional[Session] = None
|
||||
) -> list[ToolUserModel]:
|
||||
tools = self.get_tools()
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
|
||||
tools = self.get_tools(db=db)
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
}
|
||||
|
||||
return [
|
||||
tool
|
||||
|
|
@ -179,31 +192,35 @@ class ToolsTable:
|
|||
or has_access(user_id, permission, tool.access_control, user_group_ids)
|
||||
]
|
||||
|
||||
def get_tool_valves_by_id(self, id: str) -> Optional[dict]:
|
||||
def get_tool_valves_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[dict]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
tool = db.get(Tool, id)
|
||||
return tool.valves if tool.valves else {}
|
||||
except Exception as e:
|
||||
log.exception(f"Error getting tool valves by id {id}")
|
||||
return None
|
||||
|
||||
def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]:
|
||||
def update_tool_valves_by_id(
|
||||
self, id: str, valves: dict, db: Optional[Session] = None
|
||||
) -> Optional[ToolValves]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Tool).filter_by(id=id).update(
|
||||
{"valves": valves, "updated_at": int(time.time())}
|
||||
)
|
||||
db.commit()
|
||||
return self.get_tool_by_id(id)
|
||||
return self.get_tool_by_id(id, db=db)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_user_valves_by_id_and_user_id(
|
||||
self, id: str, user_id: str
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[dict]:
|
||||
try:
|
||||
user = Users.get_user_by_id(user_id)
|
||||
user = Users.get_user_by_id(user_id, db=db)
|
||||
user_settings = user.settings.model_dump() if user.settings else {}
|
||||
|
||||
# Check if user has "tools" and "valves" settings
|
||||
|
|
@ -220,10 +237,10 @@ class ToolsTable:
|
|||
return None
|
||||
|
||||
def update_user_valves_by_id_and_user_id(
|
||||
self, id: str, user_id: str, valves: dict
|
||||
self, id: str, user_id: str, valves: dict, db: Optional[Session] = None
|
||||
) -> Optional[dict]:
|
||||
try:
|
||||
user = Users.get_user_by_id(user_id)
|
||||
user = Users.get_user_by_id(user_id, db=db)
|
||||
user_settings = user.settings.model_dump() if user.settings else {}
|
||||
|
||||
# Check if user has "tools" and "valves" settings
|
||||
|
|
@ -235,7 +252,7 @@ class ToolsTable:
|
|||
user_settings["tools"]["valves"][id] = valves
|
||||
|
||||
# Update the user settings in the database
|
||||
Users.update_user_by_id(user_id, {"settings": user_settings})
|
||||
Users.update_user_by_id(user_id, {"settings": user_settings}, db=db)
|
||||
|
||||
return user_settings["tools"]["valves"][id]
|
||||
except Exception as e:
|
||||
|
|
@ -244,9 +261,11 @@ class ToolsTable:
|
|||
)
|
||||
return None
|
||||
|
||||
def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]:
|
||||
def update_tool_by_id(
|
||||
self, id: str, updated: dict, db: Optional[Session] = None
|
||||
) -> Optional[ToolModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Tool).filter_by(id=id).update(
|
||||
{**updated, "updated_at": int(time.time())}
|
||||
)
|
||||
|
|
@ -258,9 +277,9 @@ class ToolsTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def delete_tool_by_id(self, id: str) -> bool:
|
||||
def delete_tool_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Tool).filter_by(id=id).delete()
|
||||
db.commit()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import time
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.internal.db import Base, JSONField, get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
|
||||
|
||||
from open_webui.env import DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL
|
||||
|
|
@ -243,8 +244,9 @@ class UsersTable:
|
|||
profile_image_url: str = "/user.png",
|
||||
role: str = "pending",
|
||||
oauth: Optional[dict] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[UserModel]:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
user = UserModel(
|
||||
**{
|
||||
"id": id,
|
||||
|
|
@ -267,17 +269,21 @@ class UsersTable:
|
|||
else:
|
||||
return None
|
||||
|
||||
def get_user_by_id(self, id: str) -> Optional[UserModel]:
|
||||
def get_user_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).filter_by(id=id).first()
|
||||
return UserModel.model_validate(user)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
|
||||
def get_user_by_api_key(
|
||||
self, api_key: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
user = (
|
||||
db.query(User)
|
||||
.join(ApiKey, User.id == ApiKey.user_id)
|
||||
|
|
@ -288,17 +294,21 @@ class UsersTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def get_user_by_email(self, email: str) -> Optional[UserModel]:
|
||||
def get_user_by_email(
|
||||
self, email: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).filter_by(email=email).first()
|
||||
return UserModel.model_validate(user)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_user_by_oauth_sub(self, provider: str, sub: str) -> Optional[UserModel]:
|
||||
def get_user_by_oauth_sub(
|
||||
self, provider: str, sub: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db() as db: # type: Session
|
||||
with get_db_context(db) as db: # type: Session
|
||||
dialect_name = db.bind.dialect.name
|
||||
|
||||
query = db.query(User)
|
||||
|
|
@ -320,8 +330,9 @@ class UsersTable:
|
|||
filter: Optional[dict] = None,
|
||||
skip: Optional[int] = None,
|
||||
limit: Optional[int] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> dict:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
# Join GroupMember so we can order by group_id when requested
|
||||
query = db.query(User)
|
||||
|
||||
|
|
@ -452,8 +463,10 @@ class UsersTable:
|
|||
"total": total,
|
||||
}
|
||||
|
||||
def get_users_by_group_id(self, group_id: str) -> list[UserModel]:
|
||||
with get_db() as db:
|
||||
def get_users_by_group_id(
|
||||
self, group_id: str, db: Optional[Session] = None
|
||||
) -> list[UserModel]:
|
||||
with get_db_context(db) as db:
|
||||
users = (
|
||||
db.query(User)
|
||||
.join(GroupMember, User.id == GroupMember.user_id)
|
||||
|
|
@ -462,30 +475,34 @@ class UsersTable:
|
|||
)
|
||||
return [UserModel.model_validate(user) for user in users]
|
||||
|
||||
def get_users_by_user_ids(self, user_ids: list[str]) -> list[UserStatusModel]:
|
||||
with get_db() as db:
|
||||
def get_users_by_user_ids(
|
||||
self, user_ids: list[str], db: Optional[Session] = None
|
||||
) -> list[UserStatusModel]:
|
||||
with get_db_context(db) as db:
|
||||
users = db.query(User).filter(User.id.in_(user_ids)).all()
|
||||
return [UserModel.model_validate(user) for user in users]
|
||||
|
||||
def get_num_users(self) -> Optional[int]:
|
||||
with get_db() as db:
|
||||
def get_num_users(self, db: Optional[Session] = None) -> Optional[int]:
|
||||
with get_db_context(db) as db:
|
||||
return db.query(User).count()
|
||||
|
||||
def has_users(self) -> bool:
|
||||
with get_db() as db:
|
||||
def has_users(self, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
return db.query(db.query(User).exists()).scalar()
|
||||
|
||||
def get_first_user(self) -> UserModel:
|
||||
def get_first_user(self, db: Optional[Session] = None) -> UserModel:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).order_by(User.created_at).first()
|
||||
return UserModel.model_validate(user)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_user_webhook_url_by_id(self, id: str) -> Optional[str]:
|
||||
def get_user_webhook_url_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[str]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).filter_by(id=id).first()
|
||||
|
||||
if user.settings is None:
|
||||
|
|
@ -499,8 +516,8 @@ class UsersTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def get_num_users_active_today(self) -> Optional[int]:
|
||||
with get_db() as db:
|
||||
def get_num_users_active_today(self, db: Optional[Session] = None) -> Optional[int]:
|
||||
with get_db_context(db) as db:
|
||||
current_timestamp = int(datetime.datetime.now().timestamp())
|
||||
today_midnight_timestamp = current_timestamp - (current_timestamp % 86400)
|
||||
query = db.query(User).filter(
|
||||
|
|
@ -508,9 +525,11 @@ class UsersTable:
|
|||
)
|
||||
return query.count()
|
||||
|
||||
def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
|
||||
def update_user_role_by_id(
|
||||
self, id: str, role: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(User).filter_by(id=id).update({"role": role})
|
||||
db.commit()
|
||||
user = db.query(User).filter_by(id=id).first()
|
||||
|
|
@ -519,10 +538,10 @@ class UsersTable:
|
|||
return None
|
||||
|
||||
def update_user_status_by_id(
|
||||
self, id: str, form_data: UserStatus
|
||||
self, id: str, form_data: UserStatus, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(User).filter_by(id=id).update(
|
||||
{**form_data.model_dump(exclude_none=True)}
|
||||
)
|
||||
|
|
@ -534,10 +553,10 @@ class UsersTable:
|
|||
return None
|
||||
|
||||
def update_user_profile_image_url_by_id(
|
||||
self, id: str, profile_image_url: str
|
||||
self, id: str, profile_image_url: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(User).filter_by(id=id).update(
|
||||
{"profile_image_url": profile_image_url}
|
||||
)
|
||||
|
|
@ -549,9 +568,11 @@ class UsersTable:
|
|||
return None
|
||||
|
||||
@throttle(DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL)
|
||||
def update_last_active_by_id(self, id: str) -> Optional[UserModel]:
|
||||
def update_last_active_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(User).filter_by(id=id).update(
|
||||
{"last_active_at": int(time.time())}
|
||||
)
|
||||
|
|
@ -563,7 +584,7 @@ class UsersTable:
|
|||
return None
|
||||
|
||||
def update_user_oauth_by_id(
|
||||
self, id: str, provider: str, sub: str
|
||||
self, id: str, provider: str, sub: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
"""
|
||||
Update or insert an OAuth provider/sub pair into the user's oauth JSON field.
|
||||
|
|
@ -574,7 +595,7 @@ class UsersTable:
|
|||
}
|
||||
"""
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).filter_by(id=id).first()
|
||||
if not user:
|
||||
return None
|
||||
|
|
@ -594,9 +615,11 @@ class UsersTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
|
||||
def update_user_by_id(
|
||||
self, id: str, updated: dict, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(User).filter_by(id=id).update(updated)
|
||||
db.commit()
|
||||
|
||||
|
|
@ -607,10 +630,16 @@ class UsersTable:
|
|||
print(e)
|
||||
return None
|
||||
|
||||
def update_user_settings_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
|
||||
def update_user_settings_by_id(
|
||||
self, id: str, updated: dict, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
user_settings = db.query(User).filter_by(id=id).first().settings
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).filter_by(id=id).first()
|
||||
if not user:
|
||||
return None
|
||||
|
||||
user_settings = user.settings
|
||||
|
||||
if user_settings is None:
|
||||
user_settings = {}
|
||||
|
|
@ -625,15 +654,15 @@ class UsersTable:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def delete_user_by_id(self, id: str) -> bool:
|
||||
def delete_user_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
# Remove User from Groups
|
||||
Groups.remove_user_from_all_groups(id)
|
||||
|
||||
# Delete User Chats
|
||||
result = Chats.delete_chats_by_user_id(id)
|
||||
result = Chats.delete_chats_by_user_id(id, db=db)
|
||||
if result:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
# Delete User
|
||||
db.query(User).filter_by(id=id).delete()
|
||||
db.commit()
|
||||
|
|
@ -644,17 +673,21 @@ class UsersTable:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def get_user_api_key_by_id(self, id: str) -> Optional[str]:
|
||||
def get_user_api_key_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[str]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
api_key = db.query(ApiKey).filter_by(user_id=id).first()
|
||||
return api_key.key if api_key else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_user_api_key_by_id(self, id: str, api_key: str) -> bool:
|
||||
def update_user_api_key_by_id(
|
||||
self, id: str, api_key: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(ApiKey).filter_by(user_id=id).delete()
|
||||
db.commit()
|
||||
|
||||
|
|
@ -674,30 +707,32 @@ class UsersTable:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_user_api_key_by_id(self, id: str) -> bool:
|
||||
def delete_user_api_key_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
with get_db_context(db) as db:
|
||||
db.query(ApiKey).filter_by(user_id=id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_valid_user_ids(self, user_ids: list[str]) -> list[str]:
|
||||
with get_db() as db:
|
||||
def get_valid_user_ids(
|
||||
self, user_ids: list[str], db: Optional[Session] = None
|
||||
) -> list[str]:
|
||||
with get_db_context(db) as db:
|
||||
users = db.query(User).filter(User.id.in_(user_ids)).all()
|
||||
return [user.id for user in users]
|
||||
|
||||
def get_super_admin_user(self) -> Optional[UserModel]:
|
||||
with get_db() as db:
|
||||
def get_super_admin_user(self, db: Optional[Session] = None) -> Optional[UserModel]:
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).filter_by(role="admin").first()
|
||||
if user:
|
||||
return UserModel.model_validate(user)
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_active_user_count(self) -> int:
|
||||
with get_db() as db:
|
||||
def get_active_user_count(self, db: Optional[Session] = None) -> int:
|
||||
with get_db_context(db) as db:
|
||||
# Consider user active if last_active_at within the last 3 minutes
|
||||
three_minutes_ago = int(time.time()) - 180
|
||||
count = (
|
||||
|
|
@ -705,8 +740,8 @@ class UsersTable:
|
|||
)
|
||||
return count
|
||||
|
||||
def is_user_active(self, user_id: str) -> bool:
|
||||
with get_db() as db:
|
||||
def is_user_active(self, user_id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).filter_by(id=user_id).first()
|
||||
if user and user.last_active_at:
|
||||
# Consider user active if last_active_at within the last 3 minutes
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ from open_webui.retrieval.loaders.datalab_marker import DatalabMarkerLoader
|
|||
from open_webui.retrieval.loaders.mineru import MinerULoader
|
||||
|
||||
|
||||
from open_webui.env import GLOBAL_LOG_LEVEL
|
||||
from open_webui.env import GLOBAL_LOG_LEVEL, REQUESTS_VERIFY
|
||||
|
||||
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -114,7 +114,7 @@ class TikaLoader:
|
|||
endpoint += "/"
|
||||
endpoint += "tika/text"
|
||||
|
||||
r = requests.put(endpoint, data=data, headers=headers)
|
||||
r = requests.put(endpoint, data=data, headers=headers, verify=REQUESTS_VERIFY)
|
||||
|
||||
if r.ok:
|
||||
raw_metadata = r.json()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from typing import Optional, List, Tuple
|
|||
from urllib.parse import quote
|
||||
|
||||
|
||||
from open_webui.env import ENABLE_FORWARD_USER_INFO_HEADERS
|
||||
from open_webui.env import ENABLE_FORWARD_USER_INFO_HEADERS, REQUESTS_VERIFY
|
||||
from open_webui.retrieval.models.base_reranker import BaseReranker
|
||||
from open_webui.utils.headers import include_user_info_headers
|
||||
|
||||
|
|
@ -55,6 +55,7 @@ class ExternalReranker(BaseReranker):
|
|||
headers=headers,
|
||||
json=payload,
|
||||
timeout=self.timeout,
|
||||
verify=REQUESTS_VERIFY,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
|
|
|||
|
|
@ -40,8 +40,10 @@ from open_webui.retrieval.loaders.youtube import YoutubeLoader
|
|||
|
||||
|
||||
from open_webui.env import (
|
||||
AIOHTTP_CLIENT_TIMEOUT,
|
||||
OFFLINE_MODE,
|
||||
ENABLE_FORWARD_USER_INFO_HEADERS,
|
||||
AIOHTTP_CLIENT_SESSION_SSL,
|
||||
)
|
||||
from open_webui.config import (
|
||||
RAG_EMBEDDING_QUERY_PREFIX,
|
||||
|
|
@ -595,7 +597,9 @@ async def agenerate_openai_batch_embeddings(
|
|||
if ENABLE_FORWARD_USER_INFO_HEADERS and user:
|
||||
headers = include_user_info_headers(headers, user)
|
||||
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with aiohttp.ClientSession(
|
||||
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
||||
) as session:
|
||||
async with session.post(
|
||||
f"{url}/embeddings", headers=headers, json=form_data
|
||||
) as r:
|
||||
|
|
@ -684,7 +688,9 @@ async def agenerate_azure_openai_batch_embeddings(
|
|||
if ENABLE_FORWARD_USER_INFO_HEADERS and user:
|
||||
headers = include_user_info_headers(headers, user)
|
||||
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with aiohttp.ClientSession(
|
||||
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
||||
) as session:
|
||||
async with session.post(full_url, headers=headers, json=form_data) as r:
|
||||
r.raise_for_status()
|
||||
data = await r.json()
|
||||
|
|
@ -760,9 +766,14 @@ async def agenerate_ollama_batch_embeddings(
|
|||
if ENABLE_FORWARD_USER_INFO_HEADERS and user:
|
||||
headers = include_user_info_headers(headers, user)
|
||||
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with aiohttp.ClientSession(
|
||||
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
||||
) as session:
|
||||
async with session.post(
|
||||
f"{url}/api/embed", headers=headers, json=form_data
|
||||
f"{url}/api/embed",
|
||||
headers=headers,
|
||||
json=form_data,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
) as r:
|
||||
r.raise_for_status()
|
||||
data = await r.json()
|
||||
|
|
@ -791,7 +802,9 @@ def get_embedding_function(
|
|||
return await asyncio.to_thread(
|
||||
(
|
||||
lambda query, prefix=None: embedding_function.encode(
|
||||
query, **({"prompt": prefix} if prefix else {})
|
||||
query,
|
||||
batch_size=int(embedding_batch_size),
|
||||
**({"prompt": prefix} if prefix else {}),
|
||||
).tolist()
|
||||
),
|
||||
query,
|
||||
|
|
|
|||
|
|
@ -69,7 +69,11 @@ class ChromaClient(VectorDBBase):
|
|||
return self.client.delete_collection(name=collection_name)
|
||||
|
||||
def search(
|
||||
self, collection_name: str, vectors: list[list[float | int]], limit: int
|
||||
self,
|
||||
collection_name: str,
|
||||
vectors: list[list[float | int]],
|
||||
filter: Optional[dict] = None,
|
||||
limit: int = 10,
|
||||
) -> Optional[SearchResult]:
|
||||
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
|
||||
try:
|
||||
|
|
@ -78,6 +82,7 @@ class ChromaClient(VectorDBBase):
|
|||
result = collection.query(
|
||||
query_embeddings=vectors,
|
||||
n_results=limit,
|
||||
where=filter,
|
||||
)
|
||||
|
||||
# chromadb has cosine distance, 2 (worst) -> 0 (best). Re-odering to 0 -> 1
|
||||
|
|
|
|||
|
|
@ -153,7 +153,11 @@ class ElasticsearchClient(VectorDBBase):
|
|||
|
||||
# Status: works
|
||||
def search(
|
||||
self, collection_name: str, vectors: list[list[float]], limit: int
|
||||
self,
|
||||
collection_name: str,
|
||||
vectors: list[list[float]],
|
||||
filter: Optional[dict] = None,
|
||||
limit: int = 10,
|
||||
) -> Optional[SearchResult]:
|
||||
query = {
|
||||
"size": limit,
|
||||
|
|
|
|||
|
|
@ -179,7 +179,11 @@ class MilvusClient(VectorDBBase):
|
|||
)
|
||||
|
||||
def search(
|
||||
self, collection_name: str, vectors: list[list[float | int]], limit: int
|
||||
self,
|
||||
collection_name: str,
|
||||
vectors: list[list[float | int]],
|
||||
filter: Optional[dict] = None,
|
||||
limit: int = 10,
|
||||
) -> Optional[SearchResult]:
|
||||
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
|
||||
collection_name = collection_name.replace("-", "_")
|
||||
|
|
|
|||
|
|
@ -157,7 +157,11 @@ class MilvusClient(VectorDBBase):
|
|||
collection.insert(entities)
|
||||
|
||||
def search(
|
||||
self, collection_name: str, vectors: List[List[float]], limit: int
|
||||
self,
|
||||
collection_name: str,
|
||||
vectors: List[List[float]],
|
||||
filter: Optional[Dict] = None,
|
||||
limit: int = 10,
|
||||
) -> Optional[SearchResult]:
|
||||
if not vectors:
|
||||
return None
|
||||
|
|
|
|||
427
backend/open_webui/retrieval/vector/dbs/opengauss.py
Normal file
427
backend/open_webui/retrieval/vector/dbs/opengauss.py
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
from typing import Optional, List, Dict, Any
|
||||
import logging
|
||||
import re
|
||||
import json
|
||||
from sqlalchemy import (
|
||||
func,
|
||||
literal,
|
||||
cast,
|
||||
column,
|
||||
create_engine,
|
||||
Column,
|
||||
Integer,
|
||||
MetaData,
|
||||
LargeBinary,
|
||||
select,
|
||||
text,
|
||||
Text,
|
||||
Table,
|
||||
values,
|
||||
)
|
||||
from sqlalchemy.sql import true
|
||||
from sqlalchemy.pool import NullPool, QueuePool
|
||||
|
||||
from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker
|
||||
from sqlalchemy.dialects.postgresql import JSONB, array
|
||||
from pgvector.sqlalchemy import Vector
|
||||
from sqlalchemy.ext.mutable import MutableDict
|
||||
from sqlalchemy.exc import NoSuchTableError
|
||||
|
||||
from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2
|
||||
from sqlalchemy.dialects import registry
|
||||
|
||||
|
||||
class OpenGaussDialect(PGDialect_psycopg2):
|
||||
name = "opengauss"
|
||||
|
||||
def _get_server_version_info(self, connection):
|
||||
try:
|
||||
version = connection.exec_driver_sql("SELECT version()").scalar()
|
||||
if not version:
|
||||
return (9, 0, 0)
|
||||
|
||||
match = re.search(
|
||||
r"openGauss\s+(\d+)\.(\d+)\.(\d+)(?:-\w+)?", version, re.IGNORECASE
|
||||
)
|
||||
if match:
|
||||
return (int(match.group(1)), int(match.group(2)), int(match.group(3)))
|
||||
|
||||
return super()._get_server_version_info(connection)
|
||||
except Exception:
|
||||
return (9, 0, 0)
|
||||
|
||||
|
||||
# Register dialect
|
||||
registry.register("opengauss", __name__, "OpenGaussDialect")
|
||||
|
||||
from open_webui.retrieval.vector.utils import process_metadata
|
||||
from open_webui.retrieval.vector.main import (
|
||||
VectorDBBase,
|
||||
VectorItem,
|
||||
SearchResult,
|
||||
GetResult,
|
||||
)
|
||||
from open_webui.config import (
|
||||
OPENGAUSS_DB_URL,
|
||||
OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH,
|
||||
OPENGAUSS_POOL_SIZE,
|
||||
OPENGAUSS_POOL_MAX_OVERFLOW,
|
||||
OPENGAUSS_POOL_TIMEOUT,
|
||||
OPENGAUSS_POOL_RECYCLE,
|
||||
)
|
||||
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
VECTOR_LENGTH = OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH
|
||||
Base = declarative_base()
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
class DocumentChunk(Base):
|
||||
__tablename__ = "document_chunk"
|
||||
|
||||
id = Column(Text, primary_key=True)
|
||||
vector = Column(Vector(dim=VECTOR_LENGTH), nullable=True)
|
||||
collection_name = Column(Text, nullable=False)
|
||||
text = Column(Text, nullable=True)
|
||||
vmetadata = Column(MutableDict.as_mutable(JSONB), nullable=True)
|
||||
|
||||
|
||||
class OpenGaussClient(VectorDBBase):
|
||||
def __init__(self) -> None:
|
||||
if not OPENGAUSS_DB_URL:
|
||||
from open_webui.internal.db import ScopedSession
|
||||
|
||||
self.session = ScopedSession
|
||||
else:
|
||||
engine_kwargs = {"pool_pre_ping": True, "dialect": OpenGaussDialect()}
|
||||
|
||||
if isinstance(OPENGAUSS_POOL_SIZE, int) and OPENGAUSS_POOL_SIZE > 0:
|
||||
engine_kwargs.update(
|
||||
{
|
||||
"pool_size": OPENGAUSS_POOL_SIZE,
|
||||
"max_overflow": OPENGAUSS_POOL_MAX_OVERFLOW,
|
||||
"pool_timeout": OPENGAUSS_POOL_TIMEOUT,
|
||||
"pool_recycle": OPENGAUSS_POOL_RECYCLE,
|
||||
"poolclass": QueuePool,
|
||||
}
|
||||
)
|
||||
else:
|
||||
engine_kwargs["poolclass"] = NullPool
|
||||
|
||||
engine = create_engine(OPENGAUSS_DB_URL, **engine_kwargs)
|
||||
|
||||
SessionLocal = sessionmaker(
|
||||
autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
|
||||
)
|
||||
self.session = scoped_session(SessionLocal)
|
||||
|
||||
try:
|
||||
connection = self.session.connection()
|
||||
Base.metadata.create_all(bind=connection)
|
||||
|
||||
self.session.execute(
|
||||
text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_document_chunk_vector "
|
||||
"ON document_chunk USING ivfflat (vector vector_cosine_ops) WITH (lists = 100);"
|
||||
)
|
||||
)
|
||||
self.session.execute(
|
||||
text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_document_chunk_collection_name "
|
||||
"ON document_chunk (collection_name);"
|
||||
)
|
||||
)
|
||||
self.session.commit()
|
||||
log.info("OpenGauss vector database initialization completed.")
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
log.exception(f"OpenGauss Initialization failed.: {e}")
|
||||
raise
|
||||
|
||||
def check_vector_length(self) -> None:
|
||||
metadata = MetaData()
|
||||
try:
|
||||
document_chunk_table = Table(
|
||||
"document_chunk", metadata, autoload_with=self.session.bind
|
||||
)
|
||||
except NoSuchTableError:
|
||||
return
|
||||
|
||||
if "vector" in document_chunk_table.columns:
|
||||
vector_column = document_chunk_table.columns["vector"]
|
||||
vector_type = vector_column.type
|
||||
if isinstance(vector_type, Vector):
|
||||
db_vector_length = vector_type.dim
|
||||
if db_vector_length != VECTOR_LENGTH:
|
||||
raise Exception(
|
||||
f"Vector dimension mismatch: configured {VECTOR_LENGTH} vs. {db_vector_length} in the database."
|
||||
)
|
||||
else:
|
||||
raise Exception("The 'vector' column type is not Vector.")
|
||||
else:
|
||||
raise Exception(
|
||||
"The 'vector' column does not exist in the 'document_chunk' table."
|
||||
)
|
||||
|
||||
def adjust_vector_length(self, vector: List[float]) -> List[float]:
|
||||
current_length = len(vector)
|
||||
if current_length < VECTOR_LENGTH:
|
||||
vector += [0.0] * (VECTOR_LENGTH - current_length)
|
||||
elif current_length > VECTOR_LENGTH:
|
||||
vector = vector[:VECTOR_LENGTH]
|
||||
return vector
|
||||
|
||||
def insert(self, collection_name: str, items: List[VectorItem]) -> None:
|
||||
try:
|
||||
new_items = []
|
||||
for item in items:
|
||||
vector = self.adjust_vector_length(item["vector"])
|
||||
new_chunk = DocumentChunk(
|
||||
id=item["id"],
|
||||
vector=vector,
|
||||
collection_name=collection_name,
|
||||
text=item["text"],
|
||||
vmetadata=process_metadata(item["metadata"]),
|
||||
)
|
||||
new_items.append(new_chunk)
|
||||
self.session.bulk_save_objects(new_items)
|
||||
self.session.commit()
|
||||
log.info(
|
||||
f"Inserting {len(new_items)} items into collection '{collection_name}'."
|
||||
)
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
log.exception(f"Failed to insert data: {e}")
|
||||
raise
|
||||
|
||||
def upsert(self, collection_name: str, items: List[VectorItem]) -> None:
|
||||
try:
|
||||
for item in items:
|
||||
vector = self.adjust_vector_length(item["vector"])
|
||||
existing = (
|
||||
self.session.query(DocumentChunk)
|
||||
.filter(DocumentChunk.id == item["id"])
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
existing.vector = vector
|
||||
existing.text = item["text"]
|
||||
existing.vmetadata = process_metadata(item["metadata"])
|
||||
existing.collection_name = collection_name
|
||||
else:
|
||||
new_chunk = DocumentChunk(
|
||||
id=item["id"],
|
||||
vector=vector,
|
||||
collection_name=collection_name,
|
||||
text=item["text"],
|
||||
vmetadata=process_metadata(item["metadata"]),
|
||||
)
|
||||
self.session.add(new_chunk)
|
||||
self.session.commit()
|
||||
log.info(
|
||||
f"Inserting/updating {len(items)} items in collection '{collection_name}'."
|
||||
)
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
log.exception(f"Failed to insert or update data.: {e}")
|
||||
raise
|
||||
|
||||
def search(
|
||||
self,
|
||||
collection_name: str,
|
||||
vectors: List[List[float]],
|
||||
filter: Optional[Dict[str, Any]] = None,
|
||||
limit: int = 10,
|
||||
) -> Optional[SearchResult]:
|
||||
try:
|
||||
if not vectors:
|
||||
return None
|
||||
|
||||
vectors = [self.adjust_vector_length(vector) for vector in vectors]
|
||||
num_queries = len(vectors)
|
||||
|
||||
def vector_expr(vector):
|
||||
return cast(array(vector), Vector(VECTOR_LENGTH))
|
||||
|
||||
qid_col = column("qid", Integer)
|
||||
q_vector_col = column("q_vector", Vector(VECTOR_LENGTH))
|
||||
query_vectors = (
|
||||
values(qid_col, q_vector_col)
|
||||
.data(
|
||||
[(idx, vector_expr(vector)) for idx, vector in enumerate(vectors)]
|
||||
)
|
||||
.alias("query_vectors")
|
||||
)
|
||||
|
||||
result_fields = [
|
||||
DocumentChunk.id,
|
||||
DocumentChunk.text,
|
||||
DocumentChunk.vmetadata,
|
||||
(DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)).label(
|
||||
"distance"
|
||||
),
|
||||
]
|
||||
|
||||
subq = (
|
||||
select(*result_fields)
|
||||
.where(DocumentChunk.collection_name == collection_name)
|
||||
.order_by(
|
||||
DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)
|
||||
)
|
||||
)
|
||||
if limit is not None:
|
||||
subq = subq.limit(limit)
|
||||
subq = subq.lateral("result")
|
||||
|
||||
stmt = (
|
||||
select(
|
||||
query_vectors.c.qid,
|
||||
subq.c.id,
|
||||
subq.c.text,
|
||||
subq.c.vmetadata,
|
||||
subq.c.distance,
|
||||
)
|
||||
.select_from(query_vectors)
|
||||
.join(subq, true())
|
||||
.order_by(query_vectors.c.qid, subq.c.distance)
|
||||
)
|
||||
|
||||
result_proxy = self.session.execute(stmt)
|
||||
results = result_proxy.all()
|
||||
|
||||
ids = [[] for _ in range(num_queries)]
|
||||
distances = [[] for _ in range(num_queries)]
|
||||
documents = [[] for _ in range(num_queries)]
|
||||
metadatas = [[] for _ in range(num_queries)]
|
||||
|
||||
for row in results:
|
||||
qid = int(row.qid)
|
||||
ids[qid].append(row.id)
|
||||
distances[qid].append((2.0 - row.distance) / 2.0)
|
||||
documents[qid].append(row.text)
|
||||
metadatas[qid].append(row.vmetadata)
|
||||
|
||||
self.session.rollback()
|
||||
return SearchResult(
|
||||
ids=ids, distances=distances, documents=documents, metadatas=metadatas
|
||||
)
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
log.exception(f"Vector search failed: {e}")
|
||||
return None
|
||||
|
||||
def query(
|
||||
self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None
|
||||
) -> Optional[GetResult]:
|
||||
try:
|
||||
query = self.session.query(DocumentChunk).filter(
|
||||
DocumentChunk.collection_name == collection_name
|
||||
)
|
||||
|
||||
for key, value in filter.items():
|
||||
query = query.filter(DocumentChunk.vmetadata[key].astext == str(value))
|
||||
|
||||
if limit is not None:
|
||||
query = query.limit(limit)
|
||||
|
||||
results = query.all()
|
||||
|
||||
if not results:
|
||||
return None
|
||||
|
||||
ids = [[result.id for result in results]]
|
||||
documents = [[result.text for result in results]]
|
||||
metadatas = [[result.vmetadata for result in results]]
|
||||
|
||||
self.session.rollback()
|
||||
return GetResult(ids=ids, documents=documents, metadatas=metadatas)
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
log.exception(f"Conditional query failed: {e}")
|
||||
return None
|
||||
|
||||
def get(
|
||||
self, collection_name: str, limit: Optional[int] = None
|
||||
) -> Optional[GetResult]:
|
||||
try:
|
||||
query = self.session.query(DocumentChunk).filter(
|
||||
DocumentChunk.collection_name == collection_name
|
||||
)
|
||||
if limit is not None:
|
||||
query = query.limit(limit)
|
||||
|
||||
results = query.all()
|
||||
|
||||
if not results:
|
||||
return None
|
||||
|
||||
ids = [[result.id for result in results]]
|
||||
documents = [[result.text for result in results]]
|
||||
metadatas = [[result.vmetadata for result in results]]
|
||||
|
||||
self.session.rollback()
|
||||
return GetResult(ids=ids, documents=documents, metadatas=metadatas)
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
log.exception(f"Failed to retrieve data: {e}")
|
||||
return None
|
||||
|
||||
def delete(
|
||||
self,
|
||||
collection_name: str,
|
||||
ids: Optional[List[str]] = None,
|
||||
filter: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
try:
|
||||
query = self.session.query(DocumentChunk).filter(
|
||||
DocumentChunk.collection_name == collection_name
|
||||
)
|
||||
if ids:
|
||||
query = query.filter(DocumentChunk.id.in_(ids))
|
||||
if filter:
|
||||
for key, value in filter.items():
|
||||
query = query.filter(
|
||||
DocumentChunk.vmetadata[key].astext == str(value)
|
||||
)
|
||||
deleted = query.delete(synchronize_session=False)
|
||||
self.session.commit()
|
||||
log.info(f"Deleted {deleted} items from collection '{collection_name}'")
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
log.exception(f"Failed to delete data: {e}")
|
||||
raise
|
||||
|
||||
def reset(self) -> None:
|
||||
try:
|
||||
deleted = self.session.query(DocumentChunk).delete()
|
||||
self.session.commit()
|
||||
log.info(f"Reset completed. Deleted {deleted} items")
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
log.exception(f"Reset failed: {e}")
|
||||
raise
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
def has_collection(self, collection_name: str) -> bool:
|
||||
try:
|
||||
exists = (
|
||||
self.session.query(DocumentChunk)
|
||||
.filter(DocumentChunk.collection_name == collection_name)
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
self.session.rollback()
|
||||
return exists
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
log.exception(f"Failed to check collection existence: {e}")
|
||||
return False
|
||||
|
||||
def delete_collection(self, collection_name: str) -> None:
|
||||
self.delete(collection_name)
|
||||
log.info(f"Collection '{collection_name}' has been deleted")
|
||||
|
|
@ -113,7 +113,11 @@ class OpenSearchClient(VectorDBBase):
|
|||
self.client.indices.delete(index=self._get_index_name(collection_name))
|
||||
|
||||
def search(
|
||||
self, collection_name: str, vectors: list[list[float | int]], limit: int
|
||||
self,
|
||||
collection_name: str,
|
||||
vectors: list[list[float | int]],
|
||||
filter: Optional[dict] = None,
|
||||
limit: int = 10,
|
||||
) -> Optional[SearchResult]:
|
||||
try:
|
||||
if not self.has_collection(collection_name):
|
||||
|
|
|
|||
|
|
@ -521,7 +521,11 @@ class Oracle23aiClient(VectorDBBase):
|
|||
raise
|
||||
|
||||
def search(
|
||||
self, collection_name: str, vectors: List[List[Union[float, int]]], limit: int
|
||||
self,
|
||||
collection_name: str,
|
||||
vectors: List[List[Union[float, int]]],
|
||||
filter: Optional[dict] = None,
|
||||
limit: int = 10,
|
||||
) -> Optional[SearchResult]:
|
||||
"""
|
||||
Search for similar vectors in the database.
|
||||
|
|
|
|||
|
|
@ -90,9 +90,9 @@ class PgvectorClient(VectorDBBase):
|
|||
|
||||
# if no pgvector uri, use the existing database connection
|
||||
if not PGVECTOR_DB_URL:
|
||||
from open_webui.internal.db import Session
|
||||
from open_webui.internal.db import ScopedSession
|
||||
|
||||
self.session = Session
|
||||
self.session = ScopedSession
|
||||
else:
|
||||
if isinstance(PGVECTOR_POOL_SIZE, int):
|
||||
if PGVECTOR_POOL_SIZE > 0:
|
||||
|
|
@ -427,7 +427,8 @@ class PgvectorClient(VectorDBBase):
|
|||
self,
|
||||
collection_name: str,
|
||||
vectors: List[List[float]],
|
||||
limit: Optional[int] = None,
|
||||
filter: Optional[Dict[str, Any]] = None,
|
||||
limit: int = 10,
|
||||
) -> Optional[SearchResult]:
|
||||
try:
|
||||
if not vectors:
|
||||
|
|
@ -475,9 +476,47 @@ class PgvectorClient(VectorDBBase):
|
|||
)
|
||||
|
||||
# Build the lateral subquery for each query vector
|
||||
where_clauses = [DocumentChunk.collection_name == collection_name]
|
||||
|
||||
# Apply metadata filter if provided
|
||||
if filter:
|
||||
for key, value in filter.items():
|
||||
if isinstance(value, dict) and "$in" in value:
|
||||
# Handle $in operator: {"field": {"$in": [values]}}
|
||||
in_values = value["$in"]
|
||||
if PGVECTOR_PGCRYPTO:
|
||||
where_clauses.append(
|
||||
pgcrypto_decrypt(
|
||||
DocumentChunk.vmetadata,
|
||||
PGVECTOR_PGCRYPTO_KEY,
|
||||
JSONB,
|
||||
)[key].astext.in_([str(v) for v in in_values])
|
||||
)
|
||||
else:
|
||||
where_clauses.append(
|
||||
DocumentChunk.vmetadata[key].astext.in_(
|
||||
[str(v) for v in in_values]
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Handle simple equality: {"field": "value"}
|
||||
if PGVECTOR_PGCRYPTO:
|
||||
where_clauses.append(
|
||||
pgcrypto_decrypt(
|
||||
DocumentChunk.vmetadata,
|
||||
PGVECTOR_PGCRYPTO_KEY,
|
||||
JSONB,
|
||||
)[key].astext
|
||||
== str(value)
|
||||
)
|
||||
else:
|
||||
where_clauses.append(
|
||||
DocumentChunk.vmetadata[key].astext == str(value)
|
||||
)
|
||||
|
||||
subq = (
|
||||
select(*result_fields)
|
||||
.where(DocumentChunk.collection_name == collection_name)
|
||||
.where(*where_clauses)
|
||||
.order_by(
|
||||
(DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector))
|
||||
)
|
||||
|
|
|
|||
|
|
@ -391,7 +391,11 @@ class PineconeClient(VectorDBBase):
|
|||
)
|
||||
|
||||
def search(
|
||||
self, collection_name: str, vectors: List[List[Union[float, int]]], limit: int
|
||||
self,
|
||||
collection_name: str,
|
||||
vectors: List[List[Union[float, int]]],
|
||||
filter: Optional[dict] = None,
|
||||
limit: int = 10,
|
||||
) -> Optional[SearchResult]:
|
||||
"""Search for similar vectors in a collection."""
|
||||
if not vectors or not vectors[0]:
|
||||
|
|
|
|||
|
|
@ -145,7 +145,11 @@ class QdrantClient(VectorDBBase):
|
|||
)
|
||||
|
||||
def search(
|
||||
self, collection_name: str, vectors: list[list[float | int]], limit: int
|
||||
self,
|
||||
collection_name: str,
|
||||
vectors: list[list[float | int]],
|
||||
filter: Optional[dict] = None,
|
||||
limit: int = 10,
|
||||
) -> Optional[SearchResult]:
|
||||
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
|
||||
if limit is None:
|
||||
|
|
|
|||
|
|
@ -254,7 +254,11 @@ class QdrantClient(VectorDBBase):
|
|||
)
|
||||
|
||||
def search(
|
||||
self, collection_name: str, vectors: List[List[float | int]], limit: int
|
||||
self,
|
||||
collection_name: str,
|
||||
vectors: List[List[float | int]],
|
||||
filter: Optional[Dict] = None,
|
||||
limit: int = 10,
|
||||
) -> Optional[SearchResult]:
|
||||
"""
|
||||
Search for the nearest neighbor items based on the vectors with tenant isolation.
|
||||
|
|
|
|||
|
|
@ -295,7 +295,11 @@ class S3VectorClient(VectorDBBase):
|
|||
raise
|
||||
|
||||
def search(
|
||||
self, collection_name: str, vectors: List[List[Union[float, int]]], limit: int
|
||||
self,
|
||||
collection_name: str,
|
||||
vectors: List[List[Union[float, int]]],
|
||||
filter: Optional[dict] = None,
|
||||
limit: int = 10,
|
||||
) -> Optional[SearchResult]:
|
||||
"""
|
||||
Search for similar vectors in a collection using multiple query vectors.
|
||||
|
|
|
|||
|
|
@ -159,7 +159,11 @@ class WeaviateClient(VectorDBBase):
|
|||
)
|
||||
|
||||
def search(
|
||||
self, collection_name: str, vectors: List[List[Union[float, int]]], limit: int
|
||||
self,
|
||||
collection_name: str,
|
||||
vectors: List[List[Union[float, int]]],
|
||||
filter: Optional[dict] = None,
|
||||
limit: int = 10,
|
||||
) -> Optional[SearchResult]:
|
||||
sane_collection_name = self._sanitize_collection_name(collection_name)
|
||||
if not self.client.collections.exists(sane_collection_name):
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ class Vector:
|
|||
from open_webui.retrieval.vector.dbs.pgvector import PgvectorClient
|
||||
|
||||
return PgvectorClient()
|
||||
case VectorType.OPENGAUSS:
|
||||
from open_webui.retrieval.vector.dbs.opengauss import OpenGaussClient
|
||||
|
||||
return OpenGaussClient()
|
||||
case VectorType.ELASTICSEARCH:
|
||||
from open_webui.retrieval.vector.dbs.elasticsearch import (
|
||||
ElasticsearchClient,
|
||||
|
|
|
|||
|
|
@ -53,7 +53,11 @@ class VectorDBBase(ABC):
|
|||
|
||||
@abstractmethod
|
||||
def search(
|
||||
self, collection_name: str, vectors: List[List[Union[float, int]]], limit: int
|
||||
self,
|
||||
collection_name: str,
|
||||
vectors: List[List[Union[float, int]]],
|
||||
filter: Optional[Dict] = None,
|
||||
limit: int = 10,
|
||||
) -> Optional[SearchResult]:
|
||||
"""Search for similar vectors in a collection."""
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -12,3 +12,4 @@ class VectorType(StrEnum):
|
|||
ORACLE23AI = "oracle23ai"
|
||||
S3VECTOR = "s3vector"
|
||||
WEAVIATE = "weaviate"
|
||||
OPENGAUSS = "opengauss"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
|
@ -25,6 +26,14 @@ def search_brave(
|
|||
params = {"q": query, "count": count}
|
||||
|
||||
response = requests.get(url, headers=headers, params=params)
|
||||
|
||||
# Handle 429 rate limiting - Brave free tier allows 1 request/second
|
||||
# If rate limited, wait 1 second and retry once before failing
|
||||
if response.status_code == 429:
|
||||
log.info("Brave Search API rate limited (429), retrying after 1 second...")
|
||||
time.sleep(1)
|
||||
response = requests.get(url, headers=headers, params=params)
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
json_response = response.json()
|
||||
|
|
|
|||
|
|
@ -13,12 +13,14 @@ def search_duckduckgo(
|
|||
count: int,
|
||||
filter_list: Optional[list[str]] = None,
|
||||
concurrent_requests: Optional[int] = None,
|
||||
backend: Optional[str] = "auto",
|
||||
) -> list[SearchResult]:
|
||||
"""
|
||||
Search using DuckDuckGo's Search API and return the results as a list of SearchResult objects.
|
||||
Args:
|
||||
query (str): The query to search for
|
||||
count (int): The number of results to return
|
||||
backend (str): The search backend to use (auto, duckduckgo, google, brave, etc.)
|
||||
|
||||
Returns:
|
||||
list[SearchResult]: A list of search results
|
||||
|
|
@ -32,7 +34,7 @@ def search_duckduckgo(
|
|||
# Use the ddgs.text() method to perform the search
|
||||
try:
|
||||
search_results = ddgs.text(
|
||||
query, safesearch="moderate", max_results=count, backend="lite"
|
||||
query, safesearch="moderate", max_results=count, backend=backend
|
||||
)
|
||||
except RatelimitException as e:
|
||||
log.error(f"RatelimitException: {e}")
|
||||
|
|
|
|||
|
|
@ -7,17 +7,21 @@ from yarl import URL
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def search_jina(api_key: str, query: str, count: int) -> list[SearchResult]:
|
||||
def search_jina(
|
||||
api_key: str, query: str, count: int, base_url: str = ""
|
||||
) -> list[SearchResult]:
|
||||
"""
|
||||
Search using Jina's Search API and return the results as a list of SearchResult objects.
|
||||
Args:
|
||||
api_key (str): The Jina API key
|
||||
query (str): The query to search for
|
||||
count (int): The number of results to return
|
||||
base_url (str): Optional custom base URL for the Jina API
|
||||
|
||||
Returns:
|
||||
list[SearchResult]: A list of search results
|
||||
"""
|
||||
jina_search_endpoint = "https://s.jina.ai/"
|
||||
jina_search_endpoint = base_url if base_url else "https://s.jina.ai/"
|
||||
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ from open_webui.config import (
|
|||
WEB_LOADER_TIMEOUT,
|
||||
FIRECRAWL_API_BASE_URL,
|
||||
FIRECRAWL_API_KEY,
|
||||
FIRECRAWL_TIMEOUT,
|
||||
TAVILY_API_KEY,
|
||||
TAVILY_EXTRACT_DEPTH,
|
||||
EXTERNAL_WEB_LOADER_URL,
|
||||
|
|
@ -189,6 +190,7 @@ class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin):
|
|||
continue_on_failure: bool = True,
|
||||
api_key: Optional[str] = None,
|
||||
api_url: Optional[str] = None,
|
||||
timeout: Optional[int] = None,
|
||||
mode: Literal["crawl", "scrape", "map"] = "scrape",
|
||||
proxy: Optional[Dict[str, str]] = None,
|
||||
params: Optional[Dict] = None,
|
||||
|
|
@ -231,6 +233,7 @@ class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin):
|
|||
self.continue_on_failure = continue_on_failure
|
||||
self.api_key = api_key
|
||||
self.api_url = api_url
|
||||
self.timeout = timeout
|
||||
self.mode = mode
|
||||
self.params = params or {}
|
||||
|
||||
|
|
@ -253,7 +256,7 @@ class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin):
|
|||
ignore_invalid_urls=True,
|
||||
remove_base64_images=True,
|
||||
max_age=300000, # 5 minutes https://docs.firecrawl.dev/features/fast-scraping#common-maxage-values
|
||||
wait_timeout=len(self.web_paths) * 3,
|
||||
wait_timeout=self.timeout if self.timeout else len(self.web_paths) * 3,
|
||||
**self.params,
|
||||
)
|
||||
|
||||
|
|
@ -294,7 +297,7 @@ class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin):
|
|||
ignore_invalid_urls=True,
|
||||
remove_base64_images=True,
|
||||
max_age=300000, # 5 minutes https://docs.firecrawl.dev/features/fast-scraping#common-maxage-values
|
||||
wait_timeout=len(self.web_paths) * 3,
|
||||
wait_timeout=self.timeout if self.timeout else len(self.web_paths) * 3,
|
||||
**self.params,
|
||||
)
|
||||
|
||||
|
|
@ -697,6 +700,11 @@ def get_web_loader(
|
|||
WebLoaderClass = SafeFireCrawlLoader
|
||||
web_loader_args["api_key"] = FIRECRAWL_API_KEY.value
|
||||
web_loader_args["api_url"] = FIRECRAWL_API_BASE_URL.value
|
||||
if FIRECRAWL_TIMEOUT.value:
|
||||
try:
|
||||
web_loader_args["timeout"] = int(FIRECRAWL_TIMEOUT.value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if WEB_LOADER_ENGINE.value == "tavily":
|
||||
WebLoaderClass = SafeTavilyLoader
|
||||
|
|
|
|||
|
|
@ -35,12 +35,16 @@ from pydantic import BaseModel
|
|||
|
||||
from open_webui.utils.misc import strict_match_mime_type
|
||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from open_webui.utils.access_control import has_permission
|
||||
from open_webui.utils.headers import include_user_info_headers
|
||||
from open_webui.config import (
|
||||
WHISPER_MODEL_AUTO_UPDATE,
|
||||
WHISPER_COMPUTE_TYPE,
|
||||
WHISPER_MODEL_DIR,
|
||||
WHISPER_VAD_FILTER,
|
||||
CACHE_DIR,
|
||||
WHISPER_LANGUAGE,
|
||||
WHISPER_MULTILINGUAL,
|
||||
ELEVENLABS_API_BASE_URL,
|
||||
)
|
||||
|
||||
|
|
@ -129,7 +133,7 @@ def set_faster_whisper_model(model: str, auto_update: bool = False):
|
|||
faster_whisper_kwargs = {
|
||||
"model_size_or_path": model,
|
||||
"device": DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu",
|
||||
"compute_type": "int8",
|
||||
"compute_type": WHISPER_COMPUTE_TYPE,
|
||||
"download_root": WHISPER_MODEL_DIR,
|
||||
"local_files_only": not auto_update,
|
||||
}
|
||||
|
|
@ -328,6 +332,20 @@ def load_speech_pipeline(request):
|
|||
|
||||
@router.post("/speech")
|
||||
async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
if request.app.state.config.TTS_ENGINE == "":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
if user.role != "admin" and not has_permission(
|
||||
user.id, "chat.tts", request.app.state.config.USER_PERMISSIONS
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
body = await request.body()
|
||||
name = hashlib.sha256(
|
||||
body
|
||||
|
|
@ -585,8 +603,9 @@ def transcription_handler(request, file_path, metadata, user=None):
|
|||
segments, info = model.transcribe(
|
||||
file_path,
|
||||
beam_size=5,
|
||||
vad_filter=request.app.state.config.WHISPER_VAD_FILTER,
|
||||
vad_filter=WHISPER_VAD_FILTER,
|
||||
language=languages[0],
|
||||
multilingual=WHISPER_MULTILINGUAL,
|
||||
)
|
||||
log.info(
|
||||
"Detected language '%s' with probability %f"
|
||||
|
|
@ -1150,6 +1169,19 @@ def transcription(
|
|||
language: Optional[str] = Form(None),
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
if request.app.state.config.STT_ENGINE == "":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
if user.role != "admin" and not has_permission(
|
||||
user.id, "chat.stt", request.app.state.config.USER_PERMISSIONS
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
log.info(f"file.content_type: {file.content_type}")
|
||||
stt_supported_content_types = getattr(
|
||||
request.app.state.config, "STT_SUPPORTED_CONTENT_TYPES", []
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ from open_webui.utils.auth import (
|
|||
get_password_hash,
|
||||
get_http_authorization_cred,
|
||||
)
|
||||
from open_webui.internal.db import get_session
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.utils.webhook import post_webhook
|
||||
from open_webui.utils.access_control import get_permissions, has_permission
|
||||
from open_webui.utils.groups import apply_default_group_assignment
|
||||
|
|
@ -103,7 +105,10 @@ class SessionUserInfoResponse(SessionUserResponse, UserStatus):
|
|||
|
||||
@router.get("/", response_model=SessionUserInfoResponse)
|
||||
async def get_session_user(
|
||||
request: Request, response: Response, user=Depends(get_current_user)
|
||||
request: Request,
|
||||
response: Response,
|
||||
user=Depends(get_current_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
|
||||
auth_header = request.headers.get("Authorization")
|
||||
|
|
@ -137,7 +142,7 @@ async def get_session_user(
|
|||
)
|
||||
|
||||
user_permissions = get_permissions(
|
||||
user.id, request.app.state.config.USER_PERMISSIONS
|
||||
user.id, request.app.state.config.USER_PERMISSIONS, db=db
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
@ -166,12 +171,15 @@ async def get_session_user(
|
|||
|
||||
@router.post("/update/profile", response_model=UserProfileImageResponse)
|
||||
async def update_profile(
|
||||
form_data: UpdateProfileForm, session_user=Depends(get_verified_user)
|
||||
form_data: UpdateProfileForm,
|
||||
session_user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if session_user:
|
||||
user = Users.update_user_by_id(
|
||||
session_user.id,
|
||||
form_data.model_dump(),
|
||||
db=db,
|
||||
)
|
||||
if user:
|
||||
return user
|
||||
|
|
@ -181,6 +189,32 @@ async def update_profile(
|
|||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||
|
||||
|
||||
############################
|
||||
# Update Timezone
|
||||
############################
|
||||
|
||||
|
||||
class UpdateTimezoneForm(BaseModel):
|
||||
timezone: str
|
||||
|
||||
|
||||
@router.post("/update/timezone")
|
||||
async def update_timezone(
|
||||
form_data: UpdateTimezoneForm,
|
||||
session_user=Depends(get_current_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if session_user:
|
||||
Users.update_user_by_id(
|
||||
session_user.id,
|
||||
{"timezone": form_data.timezone},
|
||||
db=db,
|
||||
)
|
||||
return {"status": True}
|
||||
else:
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||
|
||||
|
||||
############################
|
||||
# Update Password
|
||||
############################
|
||||
|
|
@ -188,13 +222,17 @@ async def update_profile(
|
|||
|
||||
@router.post("/update/password", response_model=bool)
|
||||
async def update_password(
|
||||
form_data: UpdatePasswordForm, session_user=Depends(get_current_user)
|
||||
form_data: UpdatePasswordForm,
|
||||
session_user=Depends(get_current_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED)
|
||||
if session_user:
|
||||
user = Auths.authenticate_user(
|
||||
session_user.email, lambda pw: verify_password(form_data.password, pw)
|
||||
session_user.email,
|
||||
lambda pw: verify_password(form_data.password, pw),
|
||||
db=db,
|
||||
)
|
||||
|
||||
if user:
|
||||
|
|
@ -203,7 +241,7 @@ async def update_password(
|
|||
except Exception as e:
|
||||
raise HTTPException(400, detail=str(e))
|
||||
hashed = get_password_hash(form_data.new_password)
|
||||
return Auths.update_user_password_by_id(user.id, hashed)
|
||||
return Auths.update_user_password_by_id(user.id, hashed, db=db)
|
||||
else:
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.INCORRECT_PASSWORD)
|
||||
else:
|
||||
|
|
@ -214,7 +252,12 @@ async def update_password(
|
|||
# LDAP Authentication
|
||||
############################
|
||||
@router.post("/ldap", response_model=SessionUserResponse)
|
||||
async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
||||
async def ldap_auth(
|
||||
request: Request,
|
||||
response: Response,
|
||||
form_data: LdapForm,
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
# Security checks FIRST - before loading any config
|
||||
if not request.app.state.config.ENABLE_LDAP:
|
||||
raise HTTPException(400, detail="LDAP authentication is not enabled")
|
||||
|
|
@ -400,12 +443,12 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
|||
if not connection_user.bind():
|
||||
raise HTTPException(400, "Authentication failed.")
|
||||
|
||||
user = Users.get_user_by_email(email)
|
||||
user = Users.get_user_by_email(email, db=db)
|
||||
if not user:
|
||||
try:
|
||||
role = (
|
||||
"admin"
|
||||
if not Users.has_users()
|
||||
if not Users.has_users(db=db)
|
||||
else request.app.state.config.DEFAULT_USER_ROLE
|
||||
)
|
||||
|
||||
|
|
@ -414,6 +457,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
|||
password=str(uuid.uuid4()),
|
||||
name=cn,
|
||||
role=role,
|
||||
db=db,
|
||||
)
|
||||
|
||||
if not user:
|
||||
|
|
@ -424,6 +468,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
|||
apply_default_group_assignment(
|
||||
request.app.state.config.DEFAULT_GROUP_ID,
|
||||
user.id,
|
||||
db=db,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
|
|
@ -434,7 +479,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
|||
500, detail="Internal error occurred during LDAP user creation."
|
||||
)
|
||||
|
||||
user = Auths.authenticate_user_by_email(email)
|
||||
user = Auths.authenticate_user_by_email(email, db=db)
|
||||
|
||||
if user:
|
||||
expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN)
|
||||
|
|
@ -464,7 +509,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
|||
)
|
||||
|
||||
user_permissions = get_permissions(
|
||||
user.id, request.app.state.config.USER_PERMISSIONS
|
||||
user.id, request.app.state.config.USER_PERMISSIONS, db=db
|
||||
)
|
||||
|
||||
if (
|
||||
|
|
@ -473,9 +518,9 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
|||
and user_groups
|
||||
):
|
||||
if ENABLE_LDAP_GROUP_CREATION:
|
||||
Groups.create_groups_by_group_names(user.id, user_groups)
|
||||
Groups.create_groups_by_group_names(user.id, user_groups, db=db)
|
||||
try:
|
||||
Groups.sync_groups_by_group_names(user.id, user_groups)
|
||||
Groups.sync_groups_by_group_names(user.id, user_groups, db=db)
|
||||
log.info(
|
||||
f"Successfully synced groups for user {user.id}: {user_groups}"
|
||||
)
|
||||
|
|
@ -508,7 +553,12 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
|||
|
||||
|
||||
@router.post("/signin", response_model=SessionUserResponse)
|
||||
async def signin(request: Request, response: Response, form_data: SigninForm):
|
||||
async def signin(
|
||||
request: Request,
|
||||
response: Response,
|
||||
form_data: SigninForm,
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if not ENABLE_PASSWORD_AUTH:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
|
|
@ -529,14 +579,15 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
|
|||
except Exception as e:
|
||||
pass
|
||||
|
||||
if not Users.get_user_by_email(email.lower()):
|
||||
if not Users.get_user_by_email(email.lower(), db=db):
|
||||
await signup(
|
||||
request,
|
||||
response,
|
||||
SignupForm(email=email, password=str(uuid.uuid4()), name=name),
|
||||
db=db,
|
||||
)
|
||||
|
||||
user = Auths.authenticate_user_by_email(email)
|
||||
user = Auths.authenticate_user_by_email(email, db=db)
|
||||
if WEBUI_AUTH_TRUSTED_GROUPS_HEADER and user and user.role != "admin":
|
||||
group_names = request.headers.get(
|
||||
WEBUI_AUTH_TRUSTED_GROUPS_HEADER, ""
|
||||
|
|
@ -544,28 +595,33 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
|
|||
group_names = [name.strip() for name in group_names if name.strip()]
|
||||
|
||||
if group_names:
|
||||
Groups.sync_groups_by_group_names(user.id, group_names)
|
||||
Groups.sync_groups_by_group_names(user.id, group_names, db=db)
|
||||
|
||||
elif WEBUI_AUTH == False:
|
||||
admin_email = "admin@localhost"
|
||||
admin_password = "admin"
|
||||
|
||||
if Users.get_user_by_email(admin_email.lower()):
|
||||
if Users.get_user_by_email(admin_email.lower(), db=db):
|
||||
user = Auths.authenticate_user(
|
||||
admin_email.lower(), lambda pw: verify_password(admin_password, pw)
|
||||
admin_email.lower(),
|
||||
lambda pw: verify_password(admin_password, pw),
|
||||
db=db,
|
||||
)
|
||||
else:
|
||||
if Users.has_users():
|
||||
if Users.has_users(db=db):
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.EXISTING_USERS)
|
||||
|
||||
await signup(
|
||||
request,
|
||||
response,
|
||||
SignupForm(email=admin_email, password=admin_password, name="User"),
|
||||
db=db,
|
||||
)
|
||||
|
||||
user = Auths.authenticate_user(
|
||||
admin_email.lower(), lambda pw: verify_password(admin_password, pw)
|
||||
admin_email.lower(),
|
||||
lambda pw: verify_password(admin_password, pw),
|
||||
db=db,
|
||||
)
|
||||
else:
|
||||
if signin_rate_limiter.is_limited(form_data.email.lower()):
|
||||
|
|
@ -584,7 +640,9 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
|
|||
form_data.password = password_bytes.decode("utf-8", errors="ignore")
|
||||
|
||||
user = Auths.authenticate_user(
|
||||
form_data.email.lower(), lambda pw: verify_password(form_data.password, pw)
|
||||
form_data.email.lower(),
|
||||
lambda pw: verify_password(form_data.password, pw),
|
||||
db=db,
|
||||
)
|
||||
|
||||
if user:
|
||||
|
|
@ -616,7 +674,7 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
|
|||
)
|
||||
|
||||
user_permissions = get_permissions(
|
||||
user.id, request.app.state.config.USER_PERMISSIONS
|
||||
user.id, request.app.state.config.USER_PERMISSIONS, db=db
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
@ -640,8 +698,13 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
|
|||
|
||||
|
||||
@router.post("/signup", response_model=SessionUserResponse)
|
||||
async def signup(request: Request, response: Response, form_data: SignupForm):
|
||||
has_users = Users.has_users()
|
||||
async def signup(
|
||||
request: Request,
|
||||
response: Response,
|
||||
form_data: SignupForm,
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
has_users = Users.has_users(db=db)
|
||||
|
||||
if WEBUI_AUTH:
|
||||
if (
|
||||
|
|
@ -663,7 +726,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
|||
status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
|
||||
)
|
||||
|
||||
if Users.get_user_by_email(form_data.email.lower()):
|
||||
if Users.get_user_by_email(form_data.email.lower(), db=db):
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
|
||||
|
||||
try:
|
||||
|
|
@ -681,6 +744,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
|||
form_data.name,
|
||||
form_data.profile_image_url,
|
||||
role,
|
||||
db=db,
|
||||
)
|
||||
|
||||
if user:
|
||||
|
|
@ -723,7 +787,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
|||
)
|
||||
|
||||
user_permissions = get_permissions(
|
||||
user.id, request.app.state.config.USER_PERMISSIONS
|
||||
user.id, request.app.state.config.USER_PERMISSIONS, db=db
|
||||
)
|
||||
|
||||
if not has_users:
|
||||
|
|
@ -733,6 +797,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
|||
apply_default_group_assignment(
|
||||
request.app.state.config.DEFAULT_GROUP_ID,
|
||||
user.id,
|
||||
db=db,
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
@ -754,7 +819,9 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
|||
|
||||
|
||||
@router.get("/signout")
|
||||
async def signout(request: Request, response: Response):
|
||||
async def signout(
|
||||
request: Request, response: Response, db: Session = Depends(get_session)
|
||||
):
|
||||
|
||||
# get auth token from headers or cookies
|
||||
token = None
|
||||
|
|
@ -776,7 +843,7 @@ async def signout(request: Request, response: Response):
|
|||
if oauth_session_id:
|
||||
response.delete_cookie("oauth_session_id")
|
||||
|
||||
session = OAuthSessions.get_session_by_id(oauth_session_id)
|
||||
session = OAuthSessions.get_session_by_id(oauth_session_id, db=db)
|
||||
oauth_server_metadata_url = (
|
||||
request.app.state.oauth_manager.get_server_metadata_url(session.provider)
|
||||
if session
|
||||
|
|
@ -839,14 +906,17 @@ async def signout(request: Request, response: Response):
|
|||
|
||||
@router.post("/add", response_model=SigninResponse)
|
||||
async def add_user(
|
||||
request: Request, form_data: AddUserForm, user=Depends(get_admin_user)
|
||||
request: Request,
|
||||
form_data: AddUserForm,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if not validate_email_format(form_data.email.lower()):
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
|
||||
)
|
||||
|
||||
if Users.get_user_by_email(form_data.email.lower()):
|
||||
if Users.get_user_by_email(form_data.email.lower(), db=db):
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
|
||||
|
||||
try:
|
||||
|
|
@ -862,12 +932,14 @@ async def add_user(
|
|||
form_data.name,
|
||||
form_data.profile_image_url,
|
||||
form_data.role,
|
||||
db=db,
|
||||
)
|
||||
|
||||
if user:
|
||||
apply_default_group_assignment(
|
||||
request.app.state.config.DEFAULT_GROUP_ID,
|
||||
user.id,
|
||||
db=db,
|
||||
)
|
||||
|
||||
token = create_token(data={"id": user.id})
|
||||
|
|
@ -895,7 +967,9 @@ async def add_user(
|
|||
|
||||
|
||||
@router.get("/admin/details")
|
||||
async def get_admin_details(request: Request, user=Depends(get_current_user)):
|
||||
async def get_admin_details(
|
||||
request: Request, user=Depends(get_current_user), db: Session = Depends(get_session)
|
||||
):
|
||||
if request.app.state.config.SHOW_ADMIN_DETAILS:
|
||||
admin_email = request.app.state.config.ADMIN_EMAIL
|
||||
admin_name = None
|
||||
|
|
@ -903,11 +977,11 @@ async def get_admin_details(request: Request, user=Depends(get_current_user)):
|
|||
log.info(f"Admin details - Email: {admin_email}, Name: {admin_name}")
|
||||
|
||||
if admin_email:
|
||||
admin = Users.get_user_by_email(admin_email)
|
||||
admin = Users.get_user_by_email(admin_email, db=db)
|
||||
if admin:
|
||||
admin_name = admin.name
|
||||
else:
|
||||
admin = Users.get_first_user()
|
||||
admin = Users.get_first_user(db=db)
|
||||
if admin:
|
||||
admin_email = admin.email
|
||||
admin_name = admin.name
|
||||
|
|
@ -929,6 +1003,7 @@ async def get_admin_details(request: Request, user=Depends(get_current_user)):
|
|||
async def get_admin_config(request: Request, user=Depends(get_admin_user)):
|
||||
return {
|
||||
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
|
||||
"ADMIN_EMAIL": request.app.state.config.ADMIN_EMAIL,
|
||||
"WEBUI_URL": request.app.state.config.WEBUI_URL,
|
||||
"ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
|
||||
"ENABLE_API_KEYS": request.app.state.config.ENABLE_API_KEYS,
|
||||
|
|
@ -940,9 +1015,12 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
|
|||
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,
|
||||
"ENABLE_FOLDERS": request.app.state.config.ENABLE_FOLDERS,
|
||||
"FOLDER_MAX_FILE_COUNT": request.app.state.config.FOLDER_MAX_FILE_COUNT,
|
||||
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
|
||||
"ENABLE_MEMORIES": request.app.state.config.ENABLE_MEMORIES,
|
||||
"ENABLE_NOTES": request.app.state.config.ENABLE_NOTES,
|
||||
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
|
||||
"ENABLE_USER_STATUS": request.app.state.config.ENABLE_USER_STATUS,
|
||||
"PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE,
|
||||
"PENDING_USER_OVERLAY_CONTENT": request.app.state.config.PENDING_USER_OVERLAY_CONTENT,
|
||||
"RESPONSE_WATERMARK": request.app.state.config.RESPONSE_WATERMARK,
|
||||
|
|
@ -951,6 +1029,7 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
|
|||
|
||||
class AdminConfig(BaseModel):
|
||||
SHOW_ADMIN_DETAILS: bool
|
||||
ADMIN_EMAIL: Optional[str] = None
|
||||
WEBUI_URL: str
|
||||
ENABLE_SIGNUP: bool
|
||||
ENABLE_API_KEYS: bool
|
||||
|
|
@ -962,9 +1041,12 @@ class AdminConfig(BaseModel):
|
|||
ENABLE_COMMUNITY_SHARING: bool
|
||||
ENABLE_MESSAGE_RATING: bool
|
||||
ENABLE_FOLDERS: bool
|
||||
FOLDER_MAX_FILE_COUNT: Optional[int | str] = None
|
||||
ENABLE_CHANNELS: bool
|
||||
ENABLE_MEMORIES: bool
|
||||
ENABLE_NOTES: bool
|
||||
ENABLE_USER_WEBHOOKS: bool
|
||||
ENABLE_USER_STATUS: bool
|
||||
PENDING_USER_OVERLAY_TITLE: Optional[str] = None
|
||||
PENDING_USER_OVERLAY_CONTENT: Optional[str] = None
|
||||
RESPONSE_WATERMARK: Optional[str] = None
|
||||
|
|
@ -975,6 +1057,7 @@ async def update_admin_config(
|
|||
request: Request, form_data: AdminConfig, user=Depends(get_admin_user)
|
||||
):
|
||||
request.app.state.config.SHOW_ADMIN_DETAILS = form_data.SHOW_ADMIN_DETAILS
|
||||
request.app.state.config.ADMIN_EMAIL = form_data.ADMIN_EMAIL
|
||||
request.app.state.config.WEBUI_URL = form_data.WEBUI_URL
|
||||
request.app.state.config.ENABLE_SIGNUP = form_data.ENABLE_SIGNUP
|
||||
|
||||
|
|
@ -987,7 +1070,11 @@ async def update_admin_config(
|
|||
)
|
||||
|
||||
request.app.state.config.ENABLE_FOLDERS = form_data.ENABLE_FOLDERS
|
||||
request.app.state.config.FOLDER_MAX_FILE_COUNT = (
|
||||
int(form_data.FOLDER_MAX_FILE_COUNT) if form_data.FOLDER_MAX_FILE_COUNT else ""
|
||||
)
|
||||
request.app.state.config.ENABLE_CHANNELS = form_data.ENABLE_CHANNELS
|
||||
request.app.state.config.ENABLE_MEMORIES = form_data.ENABLE_MEMORIES
|
||||
request.app.state.config.ENABLE_NOTES = form_data.ENABLE_NOTES
|
||||
|
||||
if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]:
|
||||
|
|
@ -1007,6 +1094,7 @@ async def update_admin_config(
|
|||
request.app.state.config.ENABLE_MESSAGE_RATING = form_data.ENABLE_MESSAGE_RATING
|
||||
|
||||
request.app.state.config.ENABLE_USER_WEBHOOKS = form_data.ENABLE_USER_WEBHOOKS
|
||||
request.app.state.config.ENABLE_USER_STATUS = form_data.ENABLE_USER_STATUS
|
||||
|
||||
request.app.state.config.PENDING_USER_OVERLAY_TITLE = (
|
||||
form_data.PENDING_USER_OVERLAY_TITLE
|
||||
|
|
@ -1019,6 +1107,7 @@ async def update_admin_config(
|
|||
|
||||
return {
|
||||
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
|
||||
"ADMIN_EMAIL": request.app.state.config.ADMIN_EMAIL,
|
||||
"WEBUI_URL": request.app.state.config.WEBUI_URL,
|
||||
"ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
|
||||
"ENABLE_API_KEYS": request.app.state.config.ENABLE_API_KEYS,
|
||||
|
|
@ -1030,9 +1119,12 @@ async def update_admin_config(
|
|||
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,
|
||||
"ENABLE_FOLDERS": request.app.state.config.ENABLE_FOLDERS,
|
||||
"FOLDER_MAX_FILE_COUNT": request.app.state.config.FOLDER_MAX_FILE_COUNT,
|
||||
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
|
||||
"ENABLE_MEMORIES": request.app.state.config.ENABLE_MEMORIES,
|
||||
"ENABLE_NOTES": request.app.state.config.ENABLE_NOTES,
|
||||
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
|
||||
"ENABLE_USER_STATUS": request.app.state.config.ENABLE_USER_STATUS,
|
||||
"PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE,
|
||||
"PENDING_USER_OVERLAY_CONTENT": request.app.state.config.PENDING_USER_OVERLAY_CONTENT,
|
||||
"RESPONSE_WATERMARK": request.app.state.config.RESPONSE_WATERMARK,
|
||||
|
|
@ -1149,7 +1241,9 @@ async def update_ldap_config(
|
|||
|
||||
# create api key
|
||||
@router.post("/api_key", response_model=ApiKey)
|
||||
async def generate_api_key(request: Request, user=Depends(get_current_user)):
|
||||
async def generate_api_key(
|
||||
request: Request, user=Depends(get_current_user), db: Session = Depends(get_session)
|
||||
):
|
||||
if not request.app.state.config.ENABLE_API_KEYS or not has_permission(
|
||||
user.id, "features.api_keys", request.app.state.config.USER_PERMISSIONS
|
||||
):
|
||||
|
|
@ -1159,7 +1253,7 @@ async def generate_api_key(request: Request, user=Depends(get_current_user)):
|
|||
)
|
||||
|
||||
api_key = create_api_key()
|
||||
success = Users.update_user_api_key_by_id(user.id, api_key)
|
||||
success = Users.update_user_api_key_by_id(user.id, api_key, db=db)
|
||||
|
||||
if success:
|
||||
return {
|
||||
|
|
@ -1171,14 +1265,18 @@ async def generate_api_key(request: Request, user=Depends(get_current_user)):
|
|||
|
||||
# delete api key
|
||||
@router.delete("/api_key", response_model=bool)
|
||||
async def delete_api_key(user=Depends(get_current_user)):
|
||||
return Users.delete_user_api_key_by_id(user.id)
|
||||
async def delete_api_key(
|
||||
user=Depends(get_current_user), db: Session = Depends(get_session)
|
||||
):
|
||||
return Users.delete_user_api_key_by_id(user.id, db=db)
|
||||
|
||||
|
||||
# get api key
|
||||
@router.get("/api_key", response_model=ApiKey)
|
||||
async def get_api_key(user=Depends(get_current_user)):
|
||||
api_key = Users.get_user_api_key_by_id(user.id)
|
||||
async def get_api_key(
|
||||
user=Depends(get_current_user), db: Session = Depends(get_session)
|
||||
):
|
||||
api_key = Users.get_user_api_key_by_id(user.id, db=db)
|
||||
if api_key:
|
||||
return {
|
||||
"api_key": api_key,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -6,6 +6,7 @@ import aiohttp
|
|||
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.env import AIOHTTP_CLIENT_TIMEOUT
|
||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from open_webui.config import get_config, save_config
|
||||
from open_webui.config import BannerModel
|
||||
|
|
@ -228,7 +229,10 @@ async def verify_tool_servers_config(
|
|||
log.debug(
|
||||
f"Trying to fetch OAuth 2.1 discovery document from {discovery_url}"
|
||||
)
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with aiohttp.ClientSession(
|
||||
trust_env=True,
|
||||
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT),
|
||||
) as session:
|
||||
async with session.get(
|
||||
discovery_url
|
||||
) as oauth_server_metadata_response:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from typing import Optional
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from pydantic import BaseModel
|
||||
|
||||
from open_webui.models.users import Users, UserModel
|
||||
|
|
@ -10,15 +12,268 @@ from open_webui.models.feedbacks import (
|
|||
FeedbackForm,
|
||||
FeedbackUserResponse,
|
||||
FeedbackListResponse,
|
||||
LeaderboardFeedbackData,
|
||||
ModelHistoryEntry,
|
||||
ModelHistoryResponse,
|
||||
Feedbacks,
|
||||
)
|
||||
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from open_webui.internal.db import get_session
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# Leaderboard Elo Rating Computation
|
||||
#
|
||||
# How it works:
|
||||
# 1. Each model starts with a rating of 1000
|
||||
# 2. When a user picks a winner between two models, ratings are adjusted:
|
||||
# - Winner gains points, loser loses points
|
||||
# - The amount depends on expected outcome (upset = bigger change)
|
||||
# 3. The Elo formula: new_rating = old_rating + K * (actual - expected)
|
||||
# - K=32 controls how much ratings can change per match
|
||||
# - expected = probability of winning based on current ratings
|
||||
#
|
||||
# Query-based re-ranking (optional):
|
||||
# When a user searches for a topic (e.g., "coding"), we want to show
|
||||
# which models perform best FOR THAT TOPIC. We do this by:
|
||||
# 1. Computing semantic similarity between the query and each feedback's tags
|
||||
# 2. Using that similarity as a weight in the Elo calculation
|
||||
# 3. Feedbacks about "coding" contribute more to the final ranking
|
||||
# 4. Feedbacks about unrelated topics (e.g., "cooking") contribute less
|
||||
# This gives topic-specific leaderboards without needing separate data.
|
||||
|
||||
import os
|
||||
|
||||
EMBEDDING_MODEL_NAME = os.environ.get(
|
||||
"AUXILIARY_EMBEDDING_MODEL", "TaylorAI/bge-micro-v2"
|
||||
)
|
||||
_embedding_model = None
|
||||
|
||||
|
||||
def _get_embedding_model():
|
||||
global _embedding_model
|
||||
if _embedding_model is None:
|
||||
try:
|
||||
from sentence_transformers import SentenceTransformer
|
||||
|
||||
_embedding_model = SentenceTransformer(EMBEDDING_MODEL_NAME)
|
||||
except Exception as e:
|
||||
log.error(f"Embedding model load failed: {e}")
|
||||
return _embedding_model
|
||||
|
||||
|
||||
def _calculate_elo(
|
||||
feedbacks: list[LeaderboardFeedbackData], similarities: dict = None
|
||||
) -> dict:
|
||||
"""
|
||||
Calculate Elo ratings for models based on user feedback.
|
||||
|
||||
Each feedback represents a comparison where a user rated one model
|
||||
against its opponents (sibling_model_ids). Rating=1 means the model won,
|
||||
rating=-1 means it lost.
|
||||
|
||||
The Elo system adjusts ratings based on:
|
||||
- Current rating difference (upsets cause bigger swings)
|
||||
- Optional similarity weights (for query-based filtering)
|
||||
|
||||
Returns: {model_id: {"rating": float, "won": int, "lost": int}}
|
||||
"""
|
||||
K_FACTOR = 32 # Standard Elo K-factor for rating volatility
|
||||
model_stats = {}
|
||||
|
||||
def get_or_create_stats(model_id):
|
||||
if model_id not in model_stats:
|
||||
model_stats[model_id] = {"rating": 1000.0, "won": 0, "lost": 0}
|
||||
return model_stats[model_id]
|
||||
|
||||
for feedback in feedbacks:
|
||||
data = feedback.data or {}
|
||||
winner_id = data.get("model_id")
|
||||
rating_value = str(data.get("rating", ""))
|
||||
if not winner_id or rating_value not in ("1", "-1"):
|
||||
continue
|
||||
|
||||
won = rating_value == "1"
|
||||
weight = similarities.get(feedback.id, 1.0) if similarities else 1.0
|
||||
|
||||
for opponent_id in data.get("sibling_model_ids") or []:
|
||||
winner = get_or_create_stats(winner_id)
|
||||
opponent = get_or_create_stats(opponent_id)
|
||||
expected = 1 / (1 + 10 ** ((opponent["rating"] - winner["rating"]) / 400))
|
||||
|
||||
winner["rating"] += K_FACTOR * ((1 if won else 0) - expected) * weight
|
||||
opponent["rating"] += (
|
||||
K_FACTOR * ((0 if won else 1) - (1 - expected)) * weight
|
||||
)
|
||||
|
||||
if won:
|
||||
winner["won"] += 1
|
||||
opponent["lost"] += 1
|
||||
else:
|
||||
winner["lost"] += 1
|
||||
opponent["won"] += 1
|
||||
|
||||
return model_stats
|
||||
|
||||
|
||||
def _get_top_tags(feedbacks: list[LeaderboardFeedbackData], limit: int = 5) -> dict:
|
||||
"""
|
||||
Count tag occurrences per model and return the most frequent ones.
|
||||
|
||||
Each feedback can have tags describing the conversation topic.
|
||||
This aggregates those tags per model to show what topics each model
|
||||
is commonly used for.
|
||||
|
||||
Returns: {model_id: [{"tag": str, "count": int}, ...]}
|
||||
"""
|
||||
from collections import defaultdict
|
||||
|
||||
tag_counts = defaultdict(lambda: defaultdict(int))
|
||||
|
||||
for feedback in feedbacks:
|
||||
data = feedback.data or {}
|
||||
model_id = data.get("model_id")
|
||||
if model_id:
|
||||
for tag in data.get("tags", []):
|
||||
tag_counts[model_id][tag] += 1
|
||||
|
||||
return {
|
||||
model_id: [
|
||||
{"tag": tag, "count": count}
|
||||
for tag, count in sorted(tags.items(), key=lambda x: -x[1])[:limit]
|
||||
]
|
||||
for model_id, tags in tag_counts.items()
|
||||
}
|
||||
|
||||
|
||||
def _compute_similarities(feedbacks: list[LeaderboardFeedbackData], query: str) -> dict:
|
||||
"""
|
||||
Compute how relevant each feedback is to a search query.
|
||||
|
||||
Uses embeddings to find semantic similarity between the query and
|
||||
each feedback's tags. Higher similarity means the feedback is more
|
||||
relevant to what the user searched for.
|
||||
|
||||
This is used to weight Elo calculations - feedbacks matching the
|
||||
query have more influence on the final rankings.
|
||||
|
||||
Returns: {feedback_id: similarity_score (0-1)}
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
embedding_model = _get_embedding_model()
|
||||
if not embedding_model:
|
||||
return {}
|
||||
|
||||
all_tags = list(
|
||||
{
|
||||
tag
|
||||
for feedback in feedbacks
|
||||
if feedback.data
|
||||
for tag in feedback.data.get("tags", [])
|
||||
}
|
||||
)
|
||||
if not all_tags:
|
||||
return {}
|
||||
|
||||
try:
|
||||
tag_embeddings = embedding_model.encode(all_tags)
|
||||
query_embedding = embedding_model.encode([query])[0]
|
||||
except Exception as e:
|
||||
log.error(f"Embedding error: {e}")
|
||||
return {}
|
||||
|
||||
# Vectorized cosine similarity
|
||||
tag_norms = np.linalg.norm(tag_embeddings, axis=1)
|
||||
query_norm = np.linalg.norm(query_embedding)
|
||||
similarities = np.dot(tag_embeddings, query_embedding) / (
|
||||
tag_norms * query_norm + 1e-9
|
||||
)
|
||||
tag_similarity_map = dict(zip(all_tags, similarities.tolist()))
|
||||
|
||||
return {
|
||||
feedback.id: max(
|
||||
(
|
||||
tag_similarity_map.get(tag, 0)
|
||||
for tag in (feedback.data or {}).get("tags", [])
|
||||
),
|
||||
default=0,
|
||||
)
|
||||
for feedback in feedbacks
|
||||
}
|
||||
|
||||
|
||||
class LeaderboardEntry(BaseModel):
|
||||
model_id: str
|
||||
rating: int
|
||||
won: int
|
||||
lost: int
|
||||
count: int
|
||||
top_tags: list[dict]
|
||||
|
||||
|
||||
class LeaderboardResponse(BaseModel):
|
||||
entries: list[LeaderboardEntry]
|
||||
|
||||
|
||||
@router.get("/leaderboard", response_model=LeaderboardResponse)
|
||||
async def get_leaderboard(
|
||||
query: Optional[str] = None,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""Get model leaderboard with Elo ratings. Query filters by tag similarity."""
|
||||
feedbacks = Feedbacks.get_feedbacks_for_leaderboard(db=db)
|
||||
|
||||
similarities = None
|
||||
if query and query.strip():
|
||||
similarities = await run_in_threadpool(
|
||||
_compute_similarities, feedbacks, query.strip()
|
||||
)
|
||||
|
||||
elo_stats = _calculate_elo(feedbacks, similarities)
|
||||
tags_by_model = _get_top_tags(feedbacks)
|
||||
|
||||
entries = sorted(
|
||||
[
|
||||
LeaderboardEntry(
|
||||
model_id=mid,
|
||||
rating=round(s["rating"]),
|
||||
won=s["won"],
|
||||
lost=s["lost"],
|
||||
count=s["won"] + s["lost"],
|
||||
top_tags=tags_by_model.get(mid, []),
|
||||
)
|
||||
for mid, s in elo_stats.items()
|
||||
],
|
||||
key=lambda e: e.rating,
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
return LeaderboardResponse(entries=entries)
|
||||
|
||||
|
||||
@router.get("/leaderboard/{model_id}/history", response_model=ModelHistoryResponse)
|
||||
async def get_model_history(
|
||||
model_id: str,
|
||||
days: int = 30,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""Get daily win/loss history for a specific model."""
|
||||
history = Feedbacks.get_model_evaluation_history(
|
||||
model_id=model_id, days=days, db=db
|
||||
)
|
||||
return ModelHistoryResponse(model_id=model_id, history=history)
|
||||
|
||||
|
||||
############################
|
||||
# GetConfig
|
||||
############################
|
||||
|
|
@ -60,38 +315,49 @@ async def update_config(
|
|||
|
||||
|
||||
@router.get("/feedbacks/all", response_model=list[FeedbackResponse])
|
||||
async def get_all_feedbacks(user=Depends(get_admin_user)):
|
||||
feedbacks = Feedbacks.get_all_feedbacks()
|
||||
async def get_all_feedbacks(
|
||||
user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
feedbacks = Feedbacks.get_all_feedbacks(db=db)
|
||||
return feedbacks
|
||||
|
||||
|
||||
@router.get("/feedbacks/all/ids", response_model=list[FeedbackIdResponse])
|
||||
async def get_all_feedback_ids(user=Depends(get_admin_user)):
|
||||
feedbacks = Feedbacks.get_all_feedbacks()
|
||||
return feedbacks
|
||||
async def get_all_feedback_ids(
|
||||
user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
return Feedbacks.get_all_feedback_ids(db=db)
|
||||
|
||||
|
||||
@router.delete("/feedbacks/all")
|
||||
async def delete_all_feedbacks(user=Depends(get_admin_user)):
|
||||
success = Feedbacks.delete_all_feedbacks()
|
||||
async def delete_all_feedbacks(
|
||||
user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
success = Feedbacks.delete_all_feedbacks(db=db)
|
||||
return success
|
||||
|
||||
|
||||
@router.get("/feedbacks/all/export", response_model=list[FeedbackModel])
|
||||
async def export_all_feedbacks(user=Depends(get_admin_user)):
|
||||
feedbacks = Feedbacks.get_all_feedbacks()
|
||||
async def export_all_feedbacks(
|
||||
user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
feedbacks = Feedbacks.get_all_feedbacks(db=db)
|
||||
return feedbacks
|
||||
|
||||
|
||||
@router.get("/feedbacks/user", response_model=list[FeedbackUserResponse])
|
||||
async def get_feedbacks(user=Depends(get_verified_user)):
|
||||
feedbacks = Feedbacks.get_feedbacks_by_user_id(user.id)
|
||||
async def get_feedbacks(
|
||||
user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
feedbacks = Feedbacks.get_feedbacks_by_user_id(user.id, db=db)
|
||||
return feedbacks
|
||||
|
||||
|
||||
@router.delete("/feedbacks", response_model=bool)
|
||||
async def delete_feedbacks(user=Depends(get_verified_user)):
|
||||
success = Feedbacks.delete_feedbacks_by_user_id(user.id)
|
||||
async def delete_feedbacks(
|
||||
user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
success = Feedbacks.delete_feedbacks_by_user_id(user.id, db=db)
|
||||
return success
|
||||
|
||||
|
||||
|
|
@ -104,6 +370,7 @@ async def get_feedbacks(
|
|||
direction: Optional[str] = None,
|
||||
page: Optional[int] = 1,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
limit = PAGE_ITEM_COUNT
|
||||
|
||||
|
|
@ -116,7 +383,7 @@ async def get_feedbacks(
|
|||
if direction:
|
||||
filter["direction"] = direction
|
||||
|
||||
result = Feedbacks.get_feedback_items(filter=filter, skip=skip, limit=limit)
|
||||
result = Feedbacks.get_feedback_items(filter=filter, skip=skip, limit=limit, db=db)
|
||||
return result
|
||||
|
||||
|
||||
|
|
@ -125,8 +392,11 @@ async def create_feedback(
|
|||
request: Request,
|
||||
form_data: FeedbackForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
feedback = Feedbacks.insert_new_feedback(user_id=user.id, form_data=form_data)
|
||||
feedback = Feedbacks.insert_new_feedback(
|
||||
user_id=user.id, form_data=form_data, db=db
|
||||
)
|
||||
if not feedback:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -137,11 +407,15 @@ async def create_feedback(
|
|||
|
||||
|
||||
@router.get("/feedback/{id}", response_model=FeedbackModel)
|
||||
async def get_feedback_by_id(id: str, user=Depends(get_verified_user)):
|
||||
async def get_feedback_by_id(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
if user.role == "admin":
|
||||
feedback = Feedbacks.get_feedback_by_id(id=id)
|
||||
feedback = Feedbacks.get_feedback_by_id(id=id, db=db)
|
||||
else:
|
||||
feedback = Feedbacks.get_feedback_by_id_and_user_id(id=id, user_id=user.id)
|
||||
feedback = Feedbacks.get_feedback_by_id_and_user_id(
|
||||
id=id, user_id=user.id, db=db
|
||||
)
|
||||
|
||||
if not feedback:
|
||||
raise HTTPException(
|
||||
|
|
@ -153,13 +427,16 @@ async def get_feedback_by_id(id: str, user=Depends(get_verified_user)):
|
|||
|
||||
@router.post("/feedback/{id}", response_model=FeedbackModel)
|
||||
async def update_feedback_by_id(
|
||||
id: str, form_data: FeedbackForm, user=Depends(get_verified_user)
|
||||
id: str,
|
||||
form_data: FeedbackForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if user.role == "admin":
|
||||
feedback = Feedbacks.update_feedback_by_id(id=id, form_data=form_data)
|
||||
feedback = Feedbacks.update_feedback_by_id(id=id, form_data=form_data, db=db)
|
||||
else:
|
||||
feedback = Feedbacks.update_feedback_by_id_and_user_id(
|
||||
id=id, user_id=user.id, form_data=form_data
|
||||
id=id, user_id=user.id, form_data=form_data, db=db
|
||||
)
|
||||
|
||||
if not feedback:
|
||||
|
|
@ -171,11 +448,15 @@ async def update_feedback_by_id(
|
|||
|
||||
|
||||
@router.delete("/feedback/{id}")
|
||||
async def delete_feedback_by_id(id: str, user=Depends(get_verified_user)):
|
||||
async def delete_feedback_by_id(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
if user.role == "admin":
|
||||
success = Feedbacks.delete_feedback_by_id(id=id)
|
||||
success = Feedbacks.delete_feedback_by_id(id=id, db=db)
|
||||
else:
|
||||
success = Feedbacks.delete_feedback_by_id_and_user_id(id=id, user_id=user.id)
|
||||
success = Feedbacks.delete_feedback_by_id_and_user_id(
|
||||
id=id, user_id=user.id, db=db
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import logging
|
|||
import os
|
||||
import uuid
|
||||
import json
|
||||
from fnmatch import fnmatch
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from urllib.parse import quote
|
||||
|
|
@ -22,6 +21,8 @@ from fastapi import (
|
|||
)
|
||||
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import get_session, SessionLocal
|
||||
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
|
||||
|
|
@ -62,9 +63,12 @@ router = APIRouter()
|
|||
|
||||
# TODO: Optimize this function to use the knowledge_file table for faster lookups.
|
||||
def has_access_to_file(
|
||||
file_id: Optional[str], access_type: str, user=Depends(get_verified_user)
|
||||
file_id: Optional[str],
|
||||
access_type: str,
|
||||
user=Depends(get_verified_user),
|
||||
db: Optional[Session] = None,
|
||||
) -> bool:
|
||||
file = Files.get_file_by_id(file_id)
|
||||
file = Files.get_file_by_id(file_id, db=db)
|
||||
log.debug(f"Checking if user has {access_type} access to file")
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
|
|
@ -73,31 +77,33 @@ def has_access_to_file(
|
|||
)
|
||||
|
||||
# Check if the file is associated with any knowledge bases the user has access to
|
||||
knowledge_bases = Knowledges.get_knowledges_by_file_id(file_id)
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
|
||||
knowledge_bases = Knowledges.get_knowledges_by_file_id(file_id, db=db)
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user.id, db=db)
|
||||
}
|
||||
for knowledge_base in knowledge_bases:
|
||||
if knowledge_base.user_id == user.id or has_access(
|
||||
user.id, access_type, knowledge_base.access_control, user_group_ids
|
||||
user.id, access_type, knowledge_base.access_control, user_group_ids, db=db
|
||||
):
|
||||
return True
|
||||
|
||||
knowledge_base_id = file.meta.get("collection_name") if file.meta else None
|
||||
if knowledge_base_id:
|
||||
knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(
|
||||
user.id, access_type
|
||||
user.id, access_type, db=db
|
||||
)
|
||||
for knowledge_base in knowledge_bases:
|
||||
if knowledge_base.id == knowledge_base_id:
|
||||
return True
|
||||
|
||||
# Check if the file is associated with any channels the user has access to
|
||||
channels = Channels.get_channels_by_file_id_and_user_id(file_id, user.id)
|
||||
channels = Channels.get_channels_by_file_id_and_user_id(file_id, user.id, db=db)
|
||||
if access_type == "read" and channels:
|
||||
return True
|
||||
|
||||
# Check if the file is associated with any chats the user has access to
|
||||
# TODO: Granular access control for chats
|
||||
chats = Chats.get_shared_chats_by_file_id(file_id)
|
||||
chats = Chats.get_shared_chats_by_file_id(file_id, db=db)
|
||||
if chats:
|
||||
return True
|
||||
|
||||
|
|
@ -109,47 +115,78 @@ def has_access_to_file(
|
|||
############################
|
||||
|
||||
|
||||
def process_uploaded_file(request, file, file_path, file_item, file_metadata, user):
|
||||
try:
|
||||
if file.content_type:
|
||||
stt_supported_content_types = getattr(
|
||||
request.app.state.config, "STT_SUPPORTED_CONTENT_TYPES", []
|
||||
)
|
||||
def process_uploaded_file(
|
||||
request,
|
||||
file,
|
||||
file_path,
|
||||
file_item,
|
||||
file_metadata,
|
||||
user,
|
||||
db: Optional[Session] = None,
|
||||
):
|
||||
def _process_handler(db_session):
|
||||
try:
|
||||
if file.content_type:
|
||||
stt_supported_content_types = getattr(
|
||||
request.app.state.config, "STT_SUPPORTED_CONTENT_TYPES", []
|
||||
)
|
||||
|
||||
if strict_match_mime_type(stt_supported_content_types, file.content_type):
|
||||
file_path = Storage.get_file(file_path)
|
||||
result = transcribe(request, file_path, file_metadata, user)
|
||||
if strict_match_mime_type(
|
||||
stt_supported_content_types, file.content_type
|
||||
):
|
||||
file_path_processed = Storage.get_file(file_path)
|
||||
result = transcribe(
|
||||
request, file_path_processed, file_metadata, user
|
||||
)
|
||||
|
||||
process_file(
|
||||
request,
|
||||
ProcessFileForm(
|
||||
file_id=file_item.id, content=result.get("text", "")
|
||||
),
|
||||
user=user,
|
||||
db=db_session,
|
||||
)
|
||||
elif (not file.content_type.startswith(("image/", "video/"))) or (
|
||||
request.app.state.config.CONTENT_EXTRACTION_ENGINE == "external"
|
||||
):
|
||||
process_file(
|
||||
request,
|
||||
ProcessFileForm(file_id=file_item.id),
|
||||
user=user,
|
||||
db=db_session,
|
||||
)
|
||||
else:
|
||||
raise Exception(
|
||||
f"File type {file.content_type} is not supported for processing"
|
||||
)
|
||||
else:
|
||||
log.info(
|
||||
f"File type {file.content_type} is not provided, but trying to process anyway"
|
||||
)
|
||||
process_file(
|
||||
request,
|
||||
ProcessFileForm(
|
||||
file_id=file_item.id, content=result.get("text", "")
|
||||
),
|
||||
ProcessFileForm(file_id=file_item.id),
|
||||
user=user,
|
||||
db=db_session,
|
||||
)
|
||||
elif (not file.content_type.startswith(("image/", "video/"))) or (
|
||||
request.app.state.config.CONTENT_EXTRACTION_ENGINE == "external"
|
||||
):
|
||||
process_file(request, ProcessFileForm(file_id=file_item.id), user=user)
|
||||
else:
|
||||
raise Exception(
|
||||
f"File type {file.content_type} is not supported for processing"
|
||||
)
|
||||
else:
|
||||
log.info(
|
||||
f"File type {file.content_type} is not provided, but trying to process anyway"
|
||||
)
|
||||
process_file(request, ProcessFileForm(file_id=file_item.id), user=user)
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error processing file: {file_item.id}")
|
||||
Files.update_file_data_by_id(
|
||||
file_item.id,
|
||||
{
|
||||
"status": "failed",
|
||||
"error": str(e.detail) if hasattr(e, "detail") else str(e),
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f"Error processing file: {file_item.id}")
|
||||
Files.update_file_data_by_id(
|
||||
file_item.id,
|
||||
{
|
||||
"status": "failed",
|
||||
"error": str(e.detail) if hasattr(e, "detail") else str(e),
|
||||
},
|
||||
db=db_session,
|
||||
)
|
||||
|
||||
if db:
|
||||
_process_handler(db)
|
||||
else:
|
||||
with SessionLocal() as db_session:
|
||||
_process_handler(db_session)
|
||||
|
||||
|
||||
@router.post("/", response_model=FileModelResponse)
|
||||
|
|
@ -161,6 +198,7 @@ def upload_file(
|
|||
process: bool = Query(True),
|
||||
process_in_background: bool = Query(True),
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
return upload_file_handler(
|
||||
request,
|
||||
|
|
@ -170,6 +208,7 @@ def upload_file(
|
|||
process_in_background=process_in_background,
|
||||
user=user,
|
||||
background_tasks=background_tasks,
|
||||
db=db,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -181,6 +220,7 @@ def upload_file_handler(
|
|||
process_in_background: bool = Query(True),
|
||||
user=Depends(get_verified_user),
|
||||
background_tasks: Optional[BackgroundTasks] = None,
|
||||
db: Optional[Session] = None,
|
||||
):
|
||||
log.info(f"file.content_type: {file.content_type} {process}")
|
||||
|
||||
|
|
@ -248,14 +288,17 @@ def upload_file_handler(
|
|||
},
|
||||
}
|
||||
),
|
||||
db=db,
|
||||
)
|
||||
|
||||
if "channel_id" in file_metadata:
|
||||
channel = Channels.get_channel_by_id_and_user_id(
|
||||
file_metadata["channel_id"], user.id
|
||||
file_metadata["channel_id"], user.id, db=db
|
||||
)
|
||||
if channel:
|
||||
Channels.add_file_to_channel_by_id(channel.id, file_item.id, user.id)
|
||||
Channels.add_file_to_channel_by_id(
|
||||
channel.id, file_item.id, user.id, db=db
|
||||
)
|
||||
|
||||
if process:
|
||||
if background_tasks and process_in_background:
|
||||
|
|
@ -277,6 +320,7 @@ def upload_file_handler(
|
|||
file_item,
|
||||
file_metadata,
|
||||
user,
|
||||
db=db,
|
||||
)
|
||||
return {"status": True, **file_item.model_dump()}
|
||||
else:
|
||||
|
|
@ -302,11 +346,15 @@ def upload_file_handler(
|
|||
|
||||
|
||||
@router.get("/", response_model=list[FileModelResponse])
|
||||
async def list_files(user=Depends(get_verified_user), content: bool = Query(True)):
|
||||
async def list_files(
|
||||
user=Depends(get_verified_user),
|
||||
content: bool = Query(True),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if user.role == "admin":
|
||||
files = Files.get_files()
|
||||
files = Files.get_files(db=db)
|
||||
else:
|
||||
files = Files.get_files_by_user_id(user.id)
|
||||
files = Files.get_files_by_user_id(user.id, db=db)
|
||||
|
||||
if not content:
|
||||
for file in files:
|
||||
|
|
@ -328,34 +376,41 @@ async def search_files(
|
|||
description="Filename pattern to search for. Supports wildcards such as '*.txt'",
|
||||
),
|
||||
content: bool = Query(True),
|
||||
skip: int = Query(0, ge=0, description="Number of files to skip"),
|
||||
limit: int = Query(
|
||||
100, ge=1, le=1000, description="Maximum number of files to return"
|
||||
),
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""
|
||||
Search for files by filename with support for wildcard patterns.
|
||||
Uses SQL-based filtering with pagination for better performance.
|
||||
"""
|
||||
# Get files according to user role
|
||||
if user.role == "admin":
|
||||
files = Files.get_files()
|
||||
else:
|
||||
files = Files.get_files_by_user_id(user.id)
|
||||
# Determine user_id: null for admin (search all), user.id for regular users
|
||||
user_id = None if user.role == "admin" else user.id
|
||||
|
||||
# Get matching files
|
||||
matching_files = [
|
||||
file for file in files if fnmatch(file.filename.lower(), filename.lower())
|
||||
]
|
||||
# Use optimized database query with pagination
|
||||
files = Files.search_files(
|
||||
user_id=user_id,
|
||||
filename=filename,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
db=db,
|
||||
)
|
||||
|
||||
if not matching_files:
|
||||
if not files:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="No files found matching the pattern.",
|
||||
)
|
||||
|
||||
if not content:
|
||||
for file in matching_files:
|
||||
if "content" in file.data:
|
||||
for file in files:
|
||||
if file.data and "content" in file.data:
|
||||
del file.data["content"]
|
||||
|
||||
return matching_files
|
||||
return files
|
||||
|
||||
|
||||
############################
|
||||
|
|
@ -364,8 +419,10 @@ async def search_files(
|
|||
|
||||
|
||||
@router.delete("/all")
|
||||
async def delete_all_files(user=Depends(get_admin_user)):
|
||||
result = Files.delete_all_files()
|
||||
async def delete_all_files(
|
||||
user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
result = Files.delete_all_files(db=db)
|
||||
if result:
|
||||
try:
|
||||
Storage.delete_all_files()
|
||||
|
|
@ -391,8 +448,10 @@ async def delete_all_files(user=Depends(get_admin_user)):
|
|||
|
||||
|
||||
@router.get("/{id}", response_model=Optional[FileModel])
|
||||
async def get_file_by_id(id: str, user=Depends(get_verified_user)):
|
||||
file = Files.get_file_by_id(id)
|
||||
async def get_file_by_id(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
file = Files.get_file_by_id(id, db=db)
|
||||
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
|
|
@ -403,7 +462,7 @@ async def get_file_by_id(id: str, user=Depends(get_verified_user)):
|
|||
if (
|
||||
file.user_id == user.id
|
||||
or user.role == "admin"
|
||||
or has_access_to_file(id, "read", user)
|
||||
or has_access_to_file(id, "read", user, db=db)
|
||||
):
|
||||
return file
|
||||
else:
|
||||
|
|
@ -415,9 +474,12 @@ async def get_file_by_id(id: str, user=Depends(get_verified_user)):
|
|||
|
||||
@router.get("/{id}/process/status")
|
||||
async def get_file_process_status(
|
||||
id: str, stream: bool = Query(False), user=Depends(get_verified_user)
|
||||
id: str,
|
||||
stream: bool = Query(False),
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
file = Files.get_file_by_id(id)
|
||||
file = Files.get_file_by_id(id, db=db)
|
||||
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
|
|
@ -428,7 +490,7 @@ async def get_file_process_status(
|
|||
if (
|
||||
file.user_id == user.id
|
||||
or user.role == "admin"
|
||||
or has_access_to_file(id, "read", user)
|
||||
or has_access_to_file(id, "read", user, db=db)
|
||||
):
|
||||
if stream:
|
||||
MAX_FILE_PROCESSING_DURATION = 3600 * 2
|
||||
|
|
@ -436,7 +498,7 @@ async def get_file_process_status(
|
|||
async def event_stream(file_item):
|
||||
if file_item:
|
||||
for _ in range(MAX_FILE_PROCESSING_DURATION):
|
||||
file_item = Files.get_file_by_id(file_item.id)
|
||||
file_item = Files.get_file_by_id(file_item.id, db=db)
|
||||
if file_item:
|
||||
data = file_item.model_dump().get("data", {})
|
||||
status = data.get("status")
|
||||
|
|
@ -476,8 +538,10 @@ async def get_file_process_status(
|
|||
|
||||
|
||||
@router.get("/{id}/data/content")
|
||||
async def get_file_data_content_by_id(id: str, user=Depends(get_verified_user)):
|
||||
file = Files.get_file_by_id(id)
|
||||
async def get_file_data_content_by_id(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
file = Files.get_file_by_id(id, db=db)
|
||||
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
|
|
@ -488,7 +552,7 @@ async def get_file_data_content_by_id(id: str, user=Depends(get_verified_user)):
|
|||
if (
|
||||
file.user_id == user.id
|
||||
or user.role == "admin"
|
||||
or has_access_to_file(id, "read", user)
|
||||
or has_access_to_file(id, "read", user, db=db)
|
||||
):
|
||||
return {"content": file.data.get("content", "")}
|
||||
else:
|
||||
|
|
@ -509,9 +573,13 @@ class ContentForm(BaseModel):
|
|||
|
||||
@router.post("/{id}/data/content/update")
|
||||
async def update_file_data_content_by_id(
|
||||
request: Request, id: str, form_data: ContentForm, user=Depends(get_verified_user)
|
||||
request: Request,
|
||||
id: str,
|
||||
form_data: ContentForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
file = Files.get_file_by_id(id)
|
||||
file = Files.get_file_by_id(id, db=db)
|
||||
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
|
|
@ -522,7 +590,7 @@ async def update_file_data_content_by_id(
|
|||
if (
|
||||
file.user_id == user.id
|
||||
or user.role == "admin"
|
||||
or has_access_to_file(id, "write", user)
|
||||
or has_access_to_file(id, "write", user, db=db)
|
||||
):
|
||||
try:
|
||||
process_file(
|
||||
|
|
@ -530,7 +598,7 @@ async def update_file_data_content_by_id(
|
|||
ProcessFileForm(file_id=id, content=form_data.content),
|
||||
user=user,
|
||||
)
|
||||
file = Files.get_file_by_id(id=id)
|
||||
file = Files.get_file_by_id(id=id, db=db)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
log.error(f"Error processing file: {file.id}")
|
||||
|
|
@ -550,9 +618,12 @@ async def update_file_data_content_by_id(
|
|||
|
||||
@router.get("/{id}/content")
|
||||
async def get_file_content_by_id(
|
||||
id: str, user=Depends(get_verified_user), attachment: bool = Query(False)
|
||||
id: str,
|
||||
user=Depends(get_verified_user),
|
||||
attachment: bool = Query(False),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
file = Files.get_file_by_id(id)
|
||||
file = Files.get_file_by_id(id, db=db)
|
||||
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
|
|
@ -563,7 +634,7 @@ async def get_file_content_by_id(
|
|||
if (
|
||||
file.user_id == user.id
|
||||
or user.role == "admin"
|
||||
or has_access_to_file(id, "read", user)
|
||||
or has_access_to_file(id, "read", user, db=db)
|
||||
):
|
||||
try:
|
||||
file_path = Storage.get_file(file.path)
|
||||
|
|
@ -619,8 +690,10 @@ async def get_file_content_by_id(
|
|||
|
||||
|
||||
@router.get("/{id}/content/html")
|
||||
async def get_html_file_content_by_id(id: str, user=Depends(get_verified_user)):
|
||||
file = Files.get_file_by_id(id)
|
||||
async def get_html_file_content_by_id(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
file = Files.get_file_by_id(id, db=db)
|
||||
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
|
|
@ -628,7 +701,7 @@ async def get_html_file_content_by_id(id: str, user=Depends(get_verified_user)):
|
|||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
file_user = Users.get_user_by_id(file.user_id)
|
||||
file_user = Users.get_user_by_id(file.user_id, db=db)
|
||||
if not file_user.role == "admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
|
|
@ -638,7 +711,7 @@ async def get_html_file_content_by_id(id: str, user=Depends(get_verified_user)):
|
|||
if (
|
||||
file.user_id == user.id
|
||||
or user.role == "admin"
|
||||
or has_access_to_file(id, "read", user)
|
||||
or has_access_to_file(id, "read", user, db=db)
|
||||
):
|
||||
try:
|
||||
file_path = Storage.get_file(file.path)
|
||||
|
|
@ -668,8 +741,10 @@ async def get_html_file_content_by_id(id: str, user=Depends(get_verified_user)):
|
|||
|
||||
|
||||
@router.get("/{id}/content/{file_name}")
|
||||
async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
|
||||
file = Files.get_file_by_id(id)
|
||||
async def get_file_content_by_id(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
file = Files.get_file_by_id(id, db=db)
|
||||
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
|
|
@ -680,7 +755,7 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
|
|||
if (
|
||||
file.user_id == user.id
|
||||
or user.role == "admin"
|
||||
or has_access_to_file(id, "read", user)
|
||||
or has_access_to_file(id, "read", user, db=db)
|
||||
):
|
||||
file_path = file.path
|
||||
|
||||
|
|
@ -730,8 +805,10 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
|
|||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_file_by_id(id: str, user=Depends(get_verified_user)):
|
||||
file = Files.get_file_by_id(id)
|
||||
async def delete_file_by_id(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
file = Files.get_file_by_id(id, db=db)
|
||||
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
|
|
@ -742,10 +819,10 @@ async def delete_file_by_id(id: str, user=Depends(get_verified_user)):
|
|||
if (
|
||||
file.user_id == user.id
|
||||
or user.role == "admin"
|
||||
or has_access_to_file(id, "write", user)
|
||||
or has_access_to_file(id, "write", user, db=db)
|
||||
):
|
||||
|
||||
result = Files.delete_file_by_id(id)
|
||||
result = Files.delete_file_by_id(id, db=db)
|
||||
if result:
|
||||
try:
|
||||
Storage.delete_file(file.path)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ from open_webui.models.knowledge import Knowledges
|
|||
|
||||
from open_webui.config import UPLOAD_DIR
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.internal.db import get_session
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status, Request
|
||||
|
|
@ -44,7 +46,11 @@ router = APIRouter()
|
|||
|
||||
|
||||
@router.get("/", response_model=list[FolderNameIdResponse])
|
||||
async def get_folders(request: Request, user=Depends(get_verified_user)):
|
||||
async def get_folders(
|
||||
request: Request,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if request.app.state.config.ENABLE_FOLDERS is False:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
|
|
@ -55,22 +61,23 @@ async def get_folders(request: Request, user=Depends(get_verified_user)):
|
|||
user.id,
|
||||
"features.folders",
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
db=db,
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
folders = Folders.get_folders_by_user_id(user.id)
|
||||
folders = Folders.get_folders_by_user_id(user.id, db=db)
|
||||
|
||||
# Verify folder data integrity
|
||||
folder_list = []
|
||||
for folder in folders:
|
||||
if folder.parent_id and not Folders.get_folder_by_id_and_user_id(
|
||||
folder.parent_id, user.id
|
||||
folder.parent_id, user.id, db=db
|
||||
):
|
||||
folder = Folders.update_folder_parent_id_by_id_and_user_id(
|
||||
folder.id, user.id, None
|
||||
folder.id, user.id, None, db=db
|
||||
)
|
||||
|
||||
if folder.data:
|
||||
|
|
@ -80,12 +87,12 @@ async def get_folders(request: Request, user=Depends(get_verified_user)):
|
|||
|
||||
if file.get("type") == "file":
|
||||
if Files.check_access_by_user_id(
|
||||
file.get("id"), user.id, "read"
|
||||
file.get("id"), user.id, "read", db=db
|
||||
):
|
||||
valid_files.append(file)
|
||||
elif file.get("type") == "collection":
|
||||
if Knowledges.check_access_by_user_id(
|
||||
file.get("id"), user.id, "read"
|
||||
file.get("id"), user.id, "read", db=db
|
||||
):
|
||||
valid_files.append(file)
|
||||
else:
|
||||
|
|
@ -93,7 +100,7 @@ async def get_folders(request: Request, user=Depends(get_verified_user)):
|
|||
|
||||
folder.data["files"] = valid_files
|
||||
Folders.update_folder_by_id_and_user_id(
|
||||
folder.id, user.id, FolderUpdateForm(data=folder.data)
|
||||
folder.id, user.id, FolderUpdateForm(data=folder.data), db=db
|
||||
)
|
||||
|
||||
folder_list.append(FolderNameIdResponse(**folder.model_dump()))
|
||||
|
|
@ -107,9 +114,13 @@ async def get_folders(request: Request, user=Depends(get_verified_user)):
|
|||
|
||||
|
||||
@router.post("/")
|
||||
def create_folder(form_data: FolderForm, user=Depends(get_verified_user)):
|
||||
def create_folder(
|
||||
form_data: FolderForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
|
||||
None, user.id, form_data.name
|
||||
None, user.id, form_data.name, db=db
|
||||
)
|
||||
|
||||
if folder:
|
||||
|
|
@ -119,7 +130,7 @@ def create_folder(form_data: FolderForm, user=Depends(get_verified_user)):
|
|||
)
|
||||
|
||||
try:
|
||||
folder = Folders.insert_new_folder(user.id, form_data)
|
||||
folder = Folders.insert_new_folder(user.id, form_data, db=db)
|
||||
return folder
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
|
@ -136,8 +147,10 @@ def create_folder(form_data: FolderForm, user=Depends(get_verified_user)):
|
|||
|
||||
|
||||
@router.get("/{id}", response_model=Optional[FolderModel])
|
||||
async def get_folder_by_id(id: str, user=Depends(get_verified_user)):
|
||||
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
|
||||
async def get_folder_by_id(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
folder = Folders.get_folder_by_id_and_user_id(id, user.id, db=db)
|
||||
if folder:
|
||||
return folder
|
||||
else:
|
||||
|
|
@ -154,15 +167,18 @@ async def get_folder_by_id(id: str, user=Depends(get_verified_user)):
|
|||
|
||||
@router.post("/{id}/update")
|
||||
async def update_folder_name_by_id(
|
||||
id: str, form_data: FolderUpdateForm, user=Depends(get_verified_user)
|
||||
id: str,
|
||||
form_data: FolderUpdateForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
|
||||
folder = Folders.get_folder_by_id_and_user_id(id, user.id, db=db)
|
||||
if folder:
|
||||
|
||||
if form_data.name is not None:
|
||||
# Check if folder with same name exists
|
||||
existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
|
||||
folder.parent_id, user.id, form_data.name
|
||||
folder.parent_id, user.id, form_data.name, db=db
|
||||
)
|
||||
if existing_folder and existing_folder.id != id:
|
||||
raise HTTPException(
|
||||
|
|
@ -171,7 +187,9 @@ async def update_folder_name_by_id(
|
|||
)
|
||||
|
||||
try:
|
||||
folder = Folders.update_folder_by_id_and_user_id(id, user.id, form_data)
|
||||
folder = Folders.update_folder_by_id_and_user_id(
|
||||
id, user.id, form_data, db=db
|
||||
)
|
||||
return folder
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
|
@ -198,12 +216,15 @@ class FolderParentIdForm(BaseModel):
|
|||
|
||||
@router.post("/{id}/update/parent")
|
||||
async def update_folder_parent_id_by_id(
|
||||
id: str, form_data: FolderParentIdForm, user=Depends(get_verified_user)
|
||||
id: str,
|
||||
form_data: FolderParentIdForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
|
||||
folder = Folders.get_folder_by_id_and_user_id(id, user.id, db=db)
|
||||
if folder:
|
||||
existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
|
||||
form_data.parent_id, user.id, folder.name
|
||||
form_data.parent_id, user.id, folder.name, db=db
|
||||
)
|
||||
|
||||
if existing_folder:
|
||||
|
|
@ -214,7 +235,7 @@ async def update_folder_parent_id_by_id(
|
|||
|
||||
try:
|
||||
folder = Folders.update_folder_parent_id_by_id_and_user_id(
|
||||
id, user.id, form_data.parent_id
|
||||
id, user.id, form_data.parent_id, db=db
|
||||
)
|
||||
return folder
|
||||
except Exception as e:
|
||||
|
|
@ -242,13 +263,16 @@ class FolderIsExpandedForm(BaseModel):
|
|||
|
||||
@router.post("/{id}/update/expanded")
|
||||
async def update_folder_is_expanded_by_id(
|
||||
id: str, form_data: FolderIsExpandedForm, user=Depends(get_verified_user)
|
||||
id: str,
|
||||
form_data: FolderIsExpandedForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
|
||||
folder = Folders.get_folder_by_id_and_user_id(id, user.id, db=db)
|
||||
if folder:
|
||||
try:
|
||||
folder = Folders.update_folder_is_expanded_by_id_and_user_id(
|
||||
id, user.id, form_data.is_expanded
|
||||
id, user.id, form_data.is_expanded, db=db
|
||||
)
|
||||
return folder
|
||||
except Exception as e:
|
||||
|
|
@ -276,10 +300,11 @@ async def delete_folder_by_id(
|
|||
id: str,
|
||||
delete_contents: Optional[bool] = True,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if Chats.count_chats_by_folder_id_and_user_id(id, user.id):
|
||||
if Chats.count_chats_by_folder_id_and_user_id(id, user.id, db=db):
|
||||
chat_delete_permission = has_permission(
|
||||
user.id, "chat.delete", request.app.state.config.USER_PERMISSIONS
|
||||
user.id, "chat.delete", request.app.state.config.USER_PERMISSIONS, db=db
|
||||
)
|
||||
if user.role != "admin" and not chat_delete_permission:
|
||||
raise HTTPException(
|
||||
|
|
@ -288,19 +313,21 @@ async def delete_folder_by_id(
|
|||
)
|
||||
|
||||
folders = []
|
||||
folders.append(Folders.get_folder_by_id_and_user_id(id, user.id))
|
||||
folders.append(Folders.get_folder_by_id_and_user_id(id, user.id, db=db))
|
||||
while folders:
|
||||
folder = folders.pop()
|
||||
if folder:
|
||||
try:
|
||||
folder_ids = Folders.delete_folder_by_id_and_user_id(id, user.id)
|
||||
folder_ids = Folders.delete_folder_by_id_and_user_id(id, user.id, db=db)
|
||||
|
||||
for folder_id in folder_ids:
|
||||
if delete_contents:
|
||||
Chats.delete_chats_by_user_id_and_folder_id(user.id, folder_id)
|
||||
Chats.delete_chats_by_user_id_and_folder_id(
|
||||
user.id, folder_id, db=db
|
||||
)
|
||||
else:
|
||||
Chats.move_chats_by_user_id_and_folder_id(
|
||||
user.id, folder_id, None
|
||||
user.id, folder_id, None, db=db
|
||||
)
|
||||
|
||||
return True
|
||||
|
|
@ -314,7 +341,7 @@ async def delete_folder_by_id(
|
|||
finally:
|
||||
# Get all subfolders
|
||||
subfolders = Folders.get_folders_by_parent_id_and_user_id(
|
||||
folder.id, user.id
|
||||
folder.id, user.id, db=db
|
||||
)
|
||||
folders.extend(subfolders)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import aiohttp
|
|||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.env import AIOHTTP_CLIENT_TIMEOUT
|
||||
from open_webui.models.functions import (
|
||||
FunctionForm,
|
||||
FunctionModel,
|
||||
|
|
@ -24,6 +25,8 @@ from open_webui.constants import ERROR_MESSAGES
|
|||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
from open_webui.internal.db import get_session
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -37,13 +40,17 @@ router = APIRouter()
|
|||
|
||||
|
||||
@router.get("/", response_model=list[FunctionResponse])
|
||||
async def get_functions(user=Depends(get_verified_user)):
|
||||
return Functions.get_functions()
|
||||
async def get_functions(
|
||||
user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
return Functions.get_functions(db=db)
|
||||
|
||||
|
||||
@router.get("/list", response_model=list[FunctionUserResponse])
|
||||
async def get_function_list(user=Depends(get_admin_user)):
|
||||
return Functions.get_function_list()
|
||||
async def get_function_list(
|
||||
user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
return Functions.get_function_list(db=db)
|
||||
|
||||
|
||||
############################
|
||||
|
|
@ -52,8 +59,12 @@ async def get_function_list(user=Depends(get_admin_user)):
|
|||
|
||||
|
||||
@router.get("/export", response_model=list[FunctionModel | FunctionWithValvesModel])
|
||||
async def get_functions(include_valves: bool = False, user=Depends(get_admin_user)):
|
||||
return Functions.get_functions(include_valves=include_valves)
|
||||
async def get_functions(
|
||||
include_valves: bool = False,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
return Functions.get_functions(include_valves=include_valves, db=db)
|
||||
|
||||
|
||||
############################
|
||||
|
|
@ -110,7 +121,9 @@ async def load_function_from_url(
|
|||
)
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with aiohttp.ClientSession(
|
||||
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
||||
) as session:
|
||||
async with session.get(
|
||||
url, headers={"Content-Type": "application/json"}
|
||||
) as resp:
|
||||
|
|
@ -142,7 +155,10 @@ class SyncFunctionsForm(BaseModel):
|
|||
|
||||
@router.post("/sync", response_model=list[FunctionWithValvesModel])
|
||||
async def sync_functions(
|
||||
request: Request, form_data: SyncFunctionsForm, user=Depends(get_admin_user)
|
||||
request: Request,
|
||||
form_data: SyncFunctionsForm,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
for function in form_data.functions:
|
||||
|
|
@ -164,7 +180,7 @@ async def sync_functions(
|
|||
)
|
||||
raise e
|
||||
|
||||
return Functions.sync_functions(user.id, form_data.functions)
|
||||
return Functions.sync_functions(user.id, form_data.functions, db=db)
|
||||
except Exception as e:
|
||||
log.exception(f"Failed to load a function: {e}")
|
||||
raise HTTPException(
|
||||
|
|
@ -180,7 +196,10 @@ async def sync_functions(
|
|||
|
||||
@router.post("/create", response_model=Optional[FunctionResponse])
|
||||
async def create_new_function(
|
||||
request: Request, form_data: FunctionForm, user=Depends(get_admin_user)
|
||||
request: Request,
|
||||
form_data: FunctionForm,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if not form_data.id.isidentifier():
|
||||
raise HTTPException(
|
||||
|
|
@ -190,7 +209,7 @@ async def create_new_function(
|
|||
|
||||
form_data.id = form_data.id.lower()
|
||||
|
||||
function = Functions.get_function_by_id(form_data.id)
|
||||
function = Functions.get_function_by_id(form_data.id, db=db)
|
||||
if function is None:
|
||||
try:
|
||||
form_data.content = replace_imports(form_data.content)
|
||||
|
|
@ -203,13 +222,17 @@ async def create_new_function(
|
|||
FUNCTIONS = request.app.state.FUNCTIONS
|
||||
FUNCTIONS[form_data.id] = function_module
|
||||
|
||||
function = Functions.insert_new_function(user.id, function_type, form_data)
|
||||
function = Functions.insert_new_function(
|
||||
user.id, function_type, form_data, db=db
|
||||
)
|
||||
|
||||
function_cache_dir = CACHE_DIR / "functions" / form_data.id
|
||||
function_cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if function_type == "filter" and getattr(function_module, "toggle", None):
|
||||
Functions.update_function_metadata_by_id(id, {"toggle": True})
|
||||
Functions.update_function_metadata_by_id(
|
||||
form_data.id, {"toggle": True}, db=db
|
||||
)
|
||||
|
||||
if function:
|
||||
return function
|
||||
|
|
@ -237,8 +260,10 @@ async def create_new_function(
|
|||
|
||||
|
||||
@router.get("/id/{id}", response_model=Optional[FunctionModel])
|
||||
async def get_function_by_id(id: str, user=Depends(get_admin_user)):
|
||||
function = Functions.get_function_by_id(id)
|
||||
async def get_function_by_id(
|
||||
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
function = Functions.get_function_by_id(id, db=db)
|
||||
|
||||
if function:
|
||||
return function
|
||||
|
|
@ -255,11 +280,13 @@ async def get_function_by_id(id: str, user=Depends(get_admin_user)):
|
|||
|
||||
|
||||
@router.post("/id/{id}/toggle", response_model=Optional[FunctionModel])
|
||||
async def toggle_function_by_id(id: str, user=Depends(get_admin_user)):
|
||||
function = Functions.get_function_by_id(id)
|
||||
async def toggle_function_by_id(
|
||||
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
function = Functions.get_function_by_id(id, db=db)
|
||||
if function:
|
||||
function = Functions.update_function_by_id(
|
||||
id, {"is_active": not function.is_active}
|
||||
id, {"is_active": not function.is_active}, db=db
|
||||
)
|
||||
|
||||
if function:
|
||||
|
|
@ -282,11 +309,13 @@ async def toggle_function_by_id(id: str, user=Depends(get_admin_user)):
|
|||
|
||||
|
||||
@router.post("/id/{id}/toggle/global", response_model=Optional[FunctionModel])
|
||||
async def toggle_global_by_id(id: str, user=Depends(get_admin_user)):
|
||||
function = Functions.get_function_by_id(id)
|
||||
async def toggle_global_by_id(
|
||||
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
function = Functions.get_function_by_id(id, db=db)
|
||||
if function:
|
||||
function = Functions.update_function_by_id(
|
||||
id, {"is_global": not function.is_global}
|
||||
id, {"is_global": not function.is_global}, db=db
|
||||
)
|
||||
|
||||
if function:
|
||||
|
|
@ -310,7 +339,11 @@ async def toggle_global_by_id(id: str, user=Depends(get_admin_user)):
|
|||
|
||||
@router.post("/id/{id}/update", response_model=Optional[FunctionModel])
|
||||
async def update_function_by_id(
|
||||
request: Request, id: str, form_data: FunctionForm, user=Depends(get_admin_user)
|
||||
request: Request,
|
||||
id: str,
|
||||
form_data: FunctionForm,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
form_data.content = replace_imports(form_data.content)
|
||||
|
|
@ -325,10 +358,10 @@ async def update_function_by_id(
|
|||
updated = {**form_data.model_dump(exclude={"id"}), "type": function_type}
|
||||
log.debug(updated)
|
||||
|
||||
function = Functions.update_function_by_id(id, updated)
|
||||
function = Functions.update_function_by_id(id, updated, db=db)
|
||||
|
||||
if function_type == "filter" and getattr(function_module, "toggle", None):
|
||||
Functions.update_function_metadata_by_id(id, {"toggle": True})
|
||||
Functions.update_function_metadata_by_id(id, {"toggle": True}, db=db)
|
||||
|
||||
if function:
|
||||
return function
|
||||
|
|
@ -352,9 +385,12 @@ async def update_function_by_id(
|
|||
|
||||
@router.delete("/id/{id}/delete", response_model=bool)
|
||||
async def delete_function_by_id(
|
||||
request: Request, id: str, user=Depends(get_admin_user)
|
||||
request: Request,
|
||||
id: str,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
result = Functions.delete_function_by_id(id)
|
||||
result = Functions.delete_function_by_id(id, db=db)
|
||||
|
||||
if result:
|
||||
FUNCTIONS = request.app.state.FUNCTIONS
|
||||
|
|
@ -370,11 +406,13 @@ async def delete_function_by_id(
|
|||
|
||||
|
||||
@router.get("/id/{id}/valves", response_model=Optional[dict])
|
||||
async def get_function_valves_by_id(id: str, user=Depends(get_admin_user)):
|
||||
function = Functions.get_function_by_id(id)
|
||||
async def get_function_valves_by_id(
|
||||
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
function = Functions.get_function_by_id(id, db=db)
|
||||
if function:
|
||||
try:
|
||||
valves = Functions.get_function_valves_by_id(id)
|
||||
valves = Functions.get_function_valves_by_id(id, db=db)
|
||||
return valves
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
|
|
@ -395,9 +433,12 @@ async def get_function_valves_by_id(id: str, user=Depends(get_admin_user)):
|
|||
|
||||
@router.get("/id/{id}/valves/spec", response_model=Optional[dict])
|
||||
async def get_function_valves_spec_by_id(
|
||||
request: Request, id: str, user=Depends(get_admin_user)
|
||||
request: Request,
|
||||
id: str,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
function = Functions.get_function_by_id(id)
|
||||
function = Functions.get_function_by_id(id, db=db)
|
||||
if function:
|
||||
function_module, function_type, frontmatter = get_function_module_from_cache(
|
||||
request, id
|
||||
|
|
@ -421,9 +462,13 @@ async def get_function_valves_spec_by_id(
|
|||
|
||||
@router.post("/id/{id}/valves/update", response_model=Optional[dict])
|
||||
async def update_function_valves_by_id(
|
||||
request: Request, id: str, form_data: dict, user=Depends(get_admin_user)
|
||||
request: Request,
|
||||
id: str,
|
||||
form_data: dict,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
function = Functions.get_function_by_id(id)
|
||||
function = Functions.get_function_by_id(id, db=db)
|
||||
if function:
|
||||
function_module, function_type, frontmatter = get_function_module_from_cache(
|
||||
request, id
|
||||
|
|
@ -437,7 +482,7 @@ async def update_function_valves_by_id(
|
|||
valves = Valves(**form_data)
|
||||
|
||||
valves_dict = valves.model_dump(exclude_unset=True)
|
||||
Functions.update_function_valves_by_id(id, valves_dict)
|
||||
Functions.update_function_valves_by_id(id, valves_dict, db=db)
|
||||
return valves_dict
|
||||
except Exception as e:
|
||||
log.exception(f"Error updating function values by id {id}: {e}")
|
||||
|
|
@ -464,11 +509,15 @@ async def update_function_valves_by_id(
|
|||
|
||||
|
||||
@router.get("/id/{id}/valves/user", response_model=Optional[dict])
|
||||
async def get_function_user_valves_by_id(id: str, user=Depends(get_verified_user)):
|
||||
function = Functions.get_function_by_id(id)
|
||||
async def get_function_user_valves_by_id(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
function = Functions.get_function_by_id(id, db=db)
|
||||
if function:
|
||||
try:
|
||||
user_valves = Functions.get_user_valves_by_id_and_user_id(id, user.id)
|
||||
user_valves = Functions.get_user_valves_by_id_and_user_id(
|
||||
id, user.id, db=db
|
||||
)
|
||||
return user_valves
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
|
|
@ -484,9 +533,12 @@ async def get_function_user_valves_by_id(id: str, user=Depends(get_verified_user
|
|||
|
||||
@router.get("/id/{id}/valves/user/spec", response_model=Optional[dict])
|
||||
async def get_function_user_valves_spec_by_id(
|
||||
request: Request, id: str, user=Depends(get_verified_user)
|
||||
request: Request,
|
||||
id: str,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
function = Functions.get_function_by_id(id)
|
||||
function = Functions.get_function_by_id(id, db=db)
|
||||
if function:
|
||||
function_module, function_type, frontmatter = get_function_module_from_cache(
|
||||
request, id
|
||||
|
|
@ -505,9 +557,13 @@ async def get_function_user_valves_spec_by_id(
|
|||
|
||||
@router.post("/id/{id}/valves/user/update", response_model=Optional[dict])
|
||||
async def update_function_user_valves_by_id(
|
||||
request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
|
||||
request: Request,
|
||||
id: str,
|
||||
form_data: dict,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
function = Functions.get_function_by_id(id)
|
||||
function = Functions.get_function_by_id(id, db=db)
|
||||
|
||||
if function:
|
||||
function_module, function_type, frontmatter = get_function_module_from_cache(
|
||||
|
|
@ -522,7 +578,7 @@ async def update_function_user_valves_by_id(
|
|||
user_valves = UserValves(**form_data)
|
||||
user_valves_dict = user_valves.model_dump(exclude_unset=True)
|
||||
Functions.update_user_valves_by_id_and_user_id(
|
||||
id, user.id, user_valves_dict
|
||||
id, user.id, user_valves_dict, db=db
|
||||
)
|
||||
return user_valves_dict
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ from open_webui.config import CACHE_DIR
|
|||
from open_webui.constants import ERROR_MESSAGES
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
|
||||
from open_webui.internal.db import get_session
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
|
||||
|
||||
|
|
@ -29,16 +32,21 @@ router = APIRouter()
|
|||
|
||||
|
||||
@router.get("/", response_model=list[GroupResponse])
|
||||
async def get_groups(share: Optional[bool] = None, user=Depends(get_verified_user)):
|
||||
async def get_groups(
|
||||
share: Optional[bool] = None,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
|
||||
filter = {}
|
||||
|
||||
# Admins can share to all groups regardless of share setting
|
||||
if user.role != "admin":
|
||||
filter["member_id"] = user.id
|
||||
if share is not None:
|
||||
filter["share"] = share
|
||||
|
||||
if share is not None:
|
||||
filter["share"] = share
|
||||
|
||||
groups = Groups.get_groups(filter=filter)
|
||||
groups = Groups.get_groups(filter=filter, db=db)
|
||||
|
||||
return groups
|
||||
|
||||
|
|
@ -49,13 +57,17 @@ async def get_groups(share: Optional[bool] = None, user=Depends(get_verified_use
|
|||
|
||||
|
||||
@router.post("/create", response_model=Optional[GroupResponse])
|
||||
async def create_new_group(form_data: GroupForm, user=Depends(get_admin_user)):
|
||||
async def create_new_group(
|
||||
form_data: GroupForm,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
group = Groups.insert_new_group(user.id, form_data)
|
||||
group = Groups.insert_new_group(user.id, form_data, db=db)
|
||||
if group:
|
||||
return GroupResponse(
|
||||
**group.model_dump(),
|
||||
member_count=Groups.get_group_member_count_by_id(group.id),
|
||||
member_count=Groups.get_group_member_count_by_id(group.id, db=db),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
|
@ -76,12 +88,14 @@ async def create_new_group(form_data: GroupForm, user=Depends(get_admin_user)):
|
|||
|
||||
|
||||
@router.get("/id/{id}", response_model=Optional[GroupResponse])
|
||||
async def get_group_by_id(id: str, user=Depends(get_admin_user)):
|
||||
group = Groups.get_group_by_id(id)
|
||||
async def get_group_by_id(
|
||||
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
group = Groups.get_group_by_id(id, db=db)
|
||||
if group:
|
||||
return GroupResponse(
|
||||
**group.model_dump(),
|
||||
member_count=Groups.get_group_member_count_by_id(group.id),
|
||||
member_count=Groups.get_group_member_count_by_id(group.id, db=db),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
|
@ -101,13 +115,15 @@ class GroupExportResponse(GroupResponse):
|
|||
|
||||
|
||||
@router.get("/id/{id}/export", response_model=Optional[GroupExportResponse])
|
||||
async def export_group_by_id(id: str, user=Depends(get_admin_user)):
|
||||
group = Groups.get_group_by_id(id)
|
||||
async def export_group_by_id(
|
||||
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
group = Groups.get_group_by_id(id, db=db)
|
||||
if group:
|
||||
return GroupExportResponse(
|
||||
**group.model_dump(),
|
||||
member_count=Groups.get_group_member_count_by_id(group.id),
|
||||
user_ids=Groups.get_group_user_ids_by_id(group.id),
|
||||
member_count=Groups.get_group_member_count_by_id(group.id, db=db),
|
||||
user_ids=Groups.get_group_user_ids_by_id(group.id, db=db),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
|
@ -122,9 +138,11 @@ async def export_group_by_id(id: str, user=Depends(get_admin_user)):
|
|||
|
||||
|
||||
@router.post("/id/{id}/users", response_model=list[UserInfoResponse])
|
||||
async def get_users_in_group(id: str, user=Depends(get_admin_user)):
|
||||
async def get_users_in_group(
|
||||
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
try:
|
||||
users = Users.get_users_by_group_id(id)
|
||||
users = Users.get_users_by_group_id(id, db=db)
|
||||
return users
|
||||
except Exception as e:
|
||||
log.exception(f"Error adding users to group {id}: {e}")
|
||||
|
|
@ -141,14 +159,17 @@ async def get_users_in_group(id: str, user=Depends(get_admin_user)):
|
|||
|
||||
@router.post("/id/{id}/update", response_model=Optional[GroupResponse])
|
||||
async def update_group_by_id(
|
||||
id: str, form_data: GroupUpdateForm, user=Depends(get_admin_user)
|
||||
id: str,
|
||||
form_data: GroupUpdateForm,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
group = Groups.update_group_by_id(id, form_data)
|
||||
group = Groups.update_group_by_id(id, form_data, db=db)
|
||||
if group:
|
||||
return GroupResponse(
|
||||
**group.model_dump(),
|
||||
member_count=Groups.get_group_member_count_by_id(group.id),
|
||||
member_count=Groups.get_group_member_count_by_id(group.id, db=db),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
|
@ -170,17 +191,20 @@ async def update_group_by_id(
|
|||
|
||||
@router.post("/id/{id}/users/add", response_model=Optional[GroupResponse])
|
||||
async def add_user_to_group(
|
||||
id: str, form_data: UserIdsForm, user=Depends(get_admin_user)
|
||||
id: str,
|
||||
form_data: UserIdsForm,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
if form_data.user_ids:
|
||||
form_data.user_ids = Users.get_valid_user_ids(form_data.user_ids)
|
||||
form_data.user_ids = Users.get_valid_user_ids(form_data.user_ids, db=db)
|
||||
|
||||
group = Groups.add_users_to_group(id, form_data.user_ids)
|
||||
group = Groups.add_users_to_group(id, form_data.user_ids, db=db)
|
||||
if group:
|
||||
return GroupResponse(
|
||||
**group.model_dump(),
|
||||
member_count=Groups.get_group_member_count_by_id(group.id),
|
||||
member_count=Groups.get_group_member_count_by_id(group.id, db=db),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
|
@ -197,14 +221,17 @@ async def add_user_to_group(
|
|||
|
||||
@router.post("/id/{id}/users/remove", response_model=Optional[GroupResponse])
|
||||
async def remove_users_from_group(
|
||||
id: str, form_data: UserIdsForm, user=Depends(get_admin_user)
|
||||
id: str,
|
||||
form_data: UserIdsForm,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
group = Groups.remove_users_from_group(id, form_data.user_ids)
|
||||
group = Groups.remove_users_from_group(id, form_data.user_ids, db=db)
|
||||
if group:
|
||||
return GroupResponse(
|
||||
**group.model_dump(),
|
||||
member_count=Groups.get_group_member_count_by_id(group.id),
|
||||
member_count=Groups.get_group_member_count_by_id(group.id, db=db),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
|
@ -225,9 +252,11 @@ async def remove_users_from_group(
|
|||
|
||||
|
||||
@router.delete("/id/{id}/delete", response_model=bool)
|
||||
async def delete_group_by_id(id: str, user=Depends(get_admin_user)):
|
||||
async def delete_group_by_id(
|
||||
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
try:
|
||||
result = Groups.delete_group_by_id(id)
|
||||
result = Groups.delete_group_by_id(id, db=db)
|
||||
if result:
|
||||
return result
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ from open_webui.env import ENABLE_FORWARD_USER_INFO_HEADERS
|
|||
from open_webui.models.chats import Chats
|
||||
from open_webui.routers.files import upload_file_handler, get_file_content_by_id
|
||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from open_webui.utils.access_control import has_permission
|
||||
from open_webui.utils.headers import include_user_info_headers
|
||||
from open_webui.internal.db import get_session
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.utils.images.comfyui import (
|
||||
ComfyUICreateImageForm,
|
||||
ComfyUIEditImageForm,
|
||||
|
|
@ -461,6 +464,7 @@ class CreateImageForm(BaseModel):
|
|||
prompt: str
|
||||
size: Optional[str] = None
|
||||
n: int = 1
|
||||
steps: Optional[int] = None
|
||||
negative_prompt: Optional[str] = None
|
||||
|
||||
|
||||
|
|
@ -496,7 +500,7 @@ def get_image_data(data: str, headers=None):
|
|||
return None, None
|
||||
|
||||
|
||||
def upload_image(request, image_data, content_type, metadata, user):
|
||||
def upload_image(request, image_data, content_type, metadata, user, db=None):
|
||||
image_format = mimetypes.guess_extension(content_type)
|
||||
file = UploadFile(
|
||||
file=io.BytesIO(image_data),
|
||||
|
|
@ -524,6 +528,7 @@ def upload_image(request, image_data, content_type, metadata, user):
|
|||
message_id=message_id,
|
||||
file_ids=[file_item.id],
|
||||
user_id=user.id,
|
||||
db=db,
|
||||
)
|
||||
|
||||
url = request.app.url_path_for("get_file_content_by_id", id=file_item.id)
|
||||
|
|
@ -534,6 +539,20 @@ def upload_image(request, image_data, content_type, metadata, user):
|
|||
async def generate_images(
|
||||
request: Request, form_data: CreateImageForm, user=Depends(get_verified_user)
|
||||
):
|
||||
if not request.app.state.config.ENABLE_IMAGE_GENERATION:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
if user.role != "admin" and not has_permission(
|
||||
user.id, "features.image_generation", request.app.state.config.USER_PERMISSIONS
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
return await image_generations(request, form_data, user=user)
|
||||
|
||||
|
||||
|
|
@ -703,8 +722,15 @@ async def image_generations(
|
|||
"n": form_data.n,
|
||||
}
|
||||
|
||||
if request.app.state.config.IMAGE_STEPS is not None:
|
||||
data["steps"] = request.app.state.config.IMAGE_STEPS
|
||||
if (
|
||||
request.app.state.config.IMAGE_STEPS is not None
|
||||
or form_data.steps is not None
|
||||
):
|
||||
data["steps"] = (
|
||||
form_data.steps
|
||||
if form_data.steps is not None
|
||||
else request.app.state.config.IMAGE_STEPS
|
||||
)
|
||||
|
||||
if form_data.negative_prompt is not None:
|
||||
data["negative_prompt"] = form_data.negative_prompt
|
||||
|
|
@ -762,8 +788,15 @@ async def image_generations(
|
|||
"height": height,
|
||||
}
|
||||
|
||||
if request.app.state.config.IMAGE_STEPS is not None:
|
||||
data["steps"] = request.app.state.config.IMAGE_STEPS
|
||||
if (
|
||||
request.app.state.config.IMAGE_STEPS is not None
|
||||
or form_data.steps is not None
|
||||
):
|
||||
data["steps"] = (
|
||||
form_data.steps
|
||||
if form_data.steps is not None
|
||||
else request.app.state.config.IMAGE_STEPS
|
||||
)
|
||||
|
||||
if form_data.negative_prompt is not None:
|
||||
data["negative_prompt"] = form_data.negative_prompt
|
||||
|
|
@ -844,6 +877,9 @@ async def image_edits(
|
|||
try:
|
||||
|
||||
async def load_url_image(data):
|
||||
if data.startswith("data:"):
|
||||
return data
|
||||
|
||||
if data.startswith("http://") or data.startswith("https://"):
|
||||
r = await asyncio.to_thread(requests.get, data)
|
||||
r.raise_for_status()
|
||||
|
|
@ -851,10 +887,14 @@ async def image_edits(
|
|||
image_data = base64.b64encode(r.content).decode("utf-8")
|
||||
return f"data:{r.headers['content-type']};base64,{image_data}"
|
||||
|
||||
elif data.startswith("/api/v1/files"):
|
||||
file_id = data.split("/api/v1/files/")[1].split("/content")[0]
|
||||
file_response = await get_file_content_by_id(file_id, user)
|
||||
else:
|
||||
file_id = None
|
||||
if data.startswith("/api/v1/files"):
|
||||
file_id = data.split("/api/v1/files/")[1].split("/content")[0]
|
||||
else:
|
||||
file_id = data
|
||||
|
||||
file_response = await get_file_content_by_id(file_id, user)
|
||||
if isinstance(file_response, FileResponse):
|
||||
file_path = file_response.path
|
||||
|
||||
|
|
@ -864,7 +904,6 @@ async def image_edits(
|
|||
mime_type, _ = mimetypes.guess_type(file_path)
|
||||
|
||||
return f"data:{mime_type};base64,{image_data}"
|
||||
|
||||
return data
|
||||
|
||||
# Load image(s) from URL(s) if necessary
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
from typing import List, Optional
|
||||
from pydantic import BaseModel
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
import logging
|
||||
import io
|
||||
import zipfile
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import get_session
|
||||
from open_webui.models.groups import Groups
|
||||
from open_webui.models.knowledge import (
|
||||
KnowledgeFileListResponse,
|
||||
|
|
@ -23,7 +28,7 @@ from open_webui.routers.retrieval import (
|
|||
from open_webui.storage.provider import Storage
|
||||
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.utils.auth import get_verified_user
|
||||
from open_webui.utils.auth import get_verified_user, get_admin_user
|
||||
from open_webui.utils.access_control import has_access, has_permission
|
||||
|
||||
|
||||
|
|
@ -41,6 +46,54 @@ router = APIRouter()
|
|||
|
||||
PAGE_ITEM_COUNT = 30
|
||||
|
||||
############################
|
||||
# Knowledge Base Embedding
|
||||
############################
|
||||
|
||||
KNOWLEDGE_BASES_COLLECTION = "knowledge-bases"
|
||||
|
||||
|
||||
async def embed_knowledge_base_metadata(
|
||||
request: Request,
|
||||
knowledge_base_id: str,
|
||||
name: str,
|
||||
description: str,
|
||||
) -> bool:
|
||||
"""Generate and store embedding for knowledge base."""
|
||||
try:
|
||||
content = f"{name}\n\n{description}" if description else name
|
||||
embedding = await request.app.state.EMBEDDING_FUNCTION(content)
|
||||
VECTOR_DB_CLIENT.upsert(
|
||||
collection_name=KNOWLEDGE_BASES_COLLECTION,
|
||||
items=[
|
||||
{
|
||||
"id": knowledge_base_id,
|
||||
"text": content,
|
||||
"vector": embedding,
|
||||
"metadata": {
|
||||
"knowledge_base_id": knowledge_base_id,
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
log.error(f"Failed to embed knowledge base {knowledge_base_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def remove_knowledge_base_metadata_embedding(knowledge_base_id: str) -> bool:
|
||||
"""Remove knowledge base embedding."""
|
||||
try:
|
||||
VECTOR_DB_CLIENT.delete(
|
||||
collection_name=KNOWLEDGE_BASES_COLLECTION,
|
||||
ids=[knowledge_base_id],
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
log.debug(f"Failed to remove embedding for {knowledge_base_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
class KnowledgeAccessResponse(KnowledgeUserResponse):
|
||||
write_access: Optional[bool] = False
|
||||
|
|
@ -52,21 +105,25 @@ class KnowledgeAccessListResponse(BaseModel):
|
|||
|
||||
|
||||
@router.get("/", response_model=KnowledgeAccessListResponse)
|
||||
async def get_knowledge_bases(page: Optional[int] = 1, user=Depends(get_verified_user)):
|
||||
async def get_knowledge_bases(
|
||||
page: Optional[int] = 1,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
page = max(page, 1)
|
||||
limit = PAGE_ITEM_COUNT
|
||||
skip = (page - 1) * limit
|
||||
|
||||
filter = {}
|
||||
if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL:
|
||||
groups = Groups.get_groups_by_member_id(user.id)
|
||||
groups = Groups.get_groups_by_member_id(user.id, db=db)
|
||||
if groups:
|
||||
filter["group_ids"] = [group.id for group in groups]
|
||||
|
||||
filter["user_id"] = user.id
|
||||
|
||||
result = Knowledges.search_knowledge_bases(
|
||||
user.id, filter=filter, skip=skip, limit=limit
|
||||
user.id, filter=filter, skip=skip, limit=limit, db=db
|
||||
)
|
||||
|
||||
return KnowledgeAccessListResponse(
|
||||
|
|
@ -75,7 +132,10 @@ async def get_knowledge_bases(page: Optional[int] = 1, user=Depends(get_verified
|
|||
**knowledge_base.model_dump(),
|
||||
write_access=(
|
||||
user.id == knowledge_base.user_id
|
||||
or has_access(user.id, "write", knowledge_base.access_control)
|
||||
or (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
or has_access(
|
||||
user.id, "write", knowledge_base.access_control, db=db
|
||||
)
|
||||
),
|
||||
)
|
||||
for knowledge_base in result.items
|
||||
|
|
@ -90,6 +150,7 @@ async def search_knowledge_bases(
|
|||
view_option: Optional[str] = None,
|
||||
page: Optional[int] = 1,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
page = max(page, 1)
|
||||
limit = PAGE_ITEM_COUNT
|
||||
|
|
@ -102,14 +163,14 @@ async def search_knowledge_bases(
|
|||
filter["view_option"] = view_option
|
||||
|
||||
if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL:
|
||||
groups = Groups.get_groups_by_member_id(user.id)
|
||||
groups = Groups.get_groups_by_member_id(user.id, db=db)
|
||||
if groups:
|
||||
filter["group_ids"] = [group.id for group in groups]
|
||||
|
||||
filter["user_id"] = user.id
|
||||
|
||||
result = Knowledges.search_knowledge_bases(
|
||||
user.id, filter=filter, skip=skip, limit=limit
|
||||
user.id, filter=filter, skip=skip, limit=limit, db=db
|
||||
)
|
||||
|
||||
return KnowledgeAccessListResponse(
|
||||
|
|
@ -118,7 +179,10 @@ async def search_knowledge_bases(
|
|||
**knowledge_base.model_dump(),
|
||||
write_access=(
|
||||
user.id == knowledge_base.user_id
|
||||
or has_access(user.id, "write", knowledge_base.access_control)
|
||||
or (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
or has_access(
|
||||
user.id, "write", knowledge_base.access_control, db=db
|
||||
)
|
||||
),
|
||||
)
|
||||
for knowledge_base in result.items
|
||||
|
|
@ -132,6 +196,7 @@ async def search_knowledge_files(
|
|||
query: Optional[str] = None,
|
||||
page: Optional[int] = 1,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
page = max(page, 1)
|
||||
limit = PAGE_ITEM_COUNT
|
||||
|
|
@ -141,13 +206,15 @@ async def search_knowledge_files(
|
|||
if query:
|
||||
filter["query"] = query
|
||||
|
||||
groups = Groups.get_groups_by_member_id(user.id)
|
||||
groups = Groups.get_groups_by_member_id(user.id, db=db)
|
||||
if groups:
|
||||
filter["group_ids"] = [group.id for group in groups]
|
||||
|
||||
filter["user_id"] = user.id
|
||||
|
||||
return Knowledges.search_knowledge_files(filter=filter, skip=skip, limit=limit)
|
||||
return Knowledges.search_knowledge_files(
|
||||
filter=filter, skip=skip, limit=limit, db=db
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
|
|
@ -157,10 +224,13 @@ async def search_knowledge_files(
|
|||
|
||||
@router.post("/create", response_model=Optional[KnowledgeResponse])
|
||||
async def create_new_knowledge(
|
||||
request: Request, form_data: KnowledgeForm, user=Depends(get_verified_user)
|
||||
request: Request,
|
||||
form_data: KnowledgeForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if user.role != "admin" and not has_permission(
|
||||
user.id, "workspace.knowledge", request.app.state.config.USER_PERMISSIONS
|
||||
user.id, "workspace.knowledge", request.app.state.config.USER_PERMISSIONS, db=db
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -175,13 +245,21 @@ async def create_new_knowledge(
|
|||
user.id,
|
||||
"sharing.public_knowledge",
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
db=db,
|
||||
)
|
||||
):
|
||||
form_data.access_control = {}
|
||||
|
||||
knowledge = Knowledges.insert_new_knowledge(user.id, form_data)
|
||||
knowledge = Knowledges.insert_new_knowledge(user.id, form_data, db=db)
|
||||
|
||||
if knowledge:
|
||||
# Embed knowledge base for semantic search
|
||||
await embed_knowledge_base_metadata(
|
||||
request,
|
||||
knowledge.id,
|
||||
knowledge.name,
|
||||
knowledge.description,
|
||||
)
|
||||
return knowledge
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
|
@ -196,20 +274,24 @@ async def create_new_knowledge(
|
|||
|
||||
|
||||
@router.post("/reindex", response_model=bool)
|
||||
async def reindex_knowledge_files(request: Request, user=Depends(get_verified_user)):
|
||||
async def reindex_knowledge_files(
|
||||
request: Request,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if user.role != "admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||
)
|
||||
|
||||
knowledge_bases = Knowledges.get_knowledge_bases()
|
||||
knowledge_bases = Knowledges.get_knowledge_bases(db=db)
|
||||
|
||||
log.info(f"Starting reindexing for {len(knowledge_bases)} knowledge bases")
|
||||
|
||||
for knowledge_base in knowledge_bases:
|
||||
try:
|
||||
files = Knowledges.get_files_by_id(knowledge_base.id)
|
||||
files = Knowledges.get_files_by_id(knowledge_base.id, db=db)
|
||||
try:
|
||||
if VECTOR_DB_CLIENT.has_collection(collection_name=knowledge_base.id):
|
||||
VECTOR_DB_CLIENT.delete_collection(
|
||||
|
|
@ -229,6 +311,7 @@ async def reindex_knowledge_files(request: Request, user=Depends(get_verified_us
|
|||
file_id=file.id, collection_name=knowledge_base.id
|
||||
),
|
||||
user=user,
|
||||
db=db,
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(
|
||||
|
|
@ -253,6 +336,30 @@ async def reindex_knowledge_files(request: Request, user=Depends(get_verified_us
|
|||
return True
|
||||
|
||||
|
||||
############################
|
||||
# ReindexKnowledgeBases
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/metadata/reindex", response_model=dict)
|
||||
async def reindex_knowledge_base_metadata_embeddings(
|
||||
request: Request,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""Batch embed all existing knowledge bases. Admin only."""
|
||||
knowledge_bases = Knowledges.get_knowledge_bases(db=db)
|
||||
log.info(f"Reindexing embeddings for {len(knowledge_bases)} knowledge bases")
|
||||
|
||||
success_count = 0
|
||||
for kb in knowledge_bases:
|
||||
if await embed_knowledge_base_metadata(request, kb.id, kb.name, kb.description):
|
||||
success_count += 1
|
||||
|
||||
log.info(f"Embedding reindex complete: {success_count}/{len(knowledge_bases)}")
|
||||
return {"total": len(knowledge_bases), "success": success_count}
|
||||
|
||||
|
||||
############################
|
||||
# GetKnowledgeById
|
||||
############################
|
||||
|
|
@ -264,26 +371,34 @@ class KnowledgeFilesResponse(KnowledgeResponse):
|
|||
|
||||
|
||||
@router.get("/{id}", response_model=Optional[KnowledgeFilesResponse])
|
||||
async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||
async def get_knowledge_by_id(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id, db=db)
|
||||
|
||||
if knowledge:
|
||||
if (
|
||||
user.role == "admin"
|
||||
or knowledge.user_id == user.id
|
||||
or has_access(user.id, "read", knowledge.access_control)
|
||||
or has_access(user.id, "read", knowledge.access_control, db=db)
|
||||
):
|
||||
|
||||
return KnowledgeFilesResponse(
|
||||
**knowledge.model_dump(),
|
||||
write_access=(
|
||||
user.id == knowledge.user_id
|
||||
or has_access(user.id, "write", knowledge.access_control)
|
||||
or (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
or has_access(user.id, "write", knowledge.access_control, db=db)
|
||||
),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
|
@ -299,8 +414,9 @@ async def update_knowledge_by_id(
|
|||
id: str,
|
||||
form_data: KnowledgeForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id, db=db)
|
||||
if not knowledge:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -309,7 +425,7 @@ async def update_knowledge_by_id(
|
|||
# Is the user the original creator, in a group with write access, or an admin
|
||||
if (
|
||||
knowledge.user_id != user.id
|
||||
and not has_access(user.id, "write", knowledge.access_control)
|
||||
and not has_access(user.id, "write", knowledge.access_control, db=db)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
@ -325,15 +441,23 @@ async def update_knowledge_by_id(
|
|||
user.id,
|
||||
"sharing.public_knowledge",
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
db=db,
|
||||
)
|
||||
):
|
||||
form_data.access_control = {}
|
||||
|
||||
knowledge = Knowledges.update_knowledge_by_id(id=id, form_data=form_data)
|
||||
knowledge = Knowledges.update_knowledge_by_id(id=id, form_data=form_data, db=db)
|
||||
if knowledge:
|
||||
# Re-embed knowledge base for semantic search
|
||||
await embed_knowledge_base_metadata(
|
||||
request,
|
||||
knowledge.id,
|
||||
knowledge.name,
|
||||
knowledge.description,
|
||||
)
|
||||
return KnowledgeFilesResponse(
|
||||
**knowledge.model_dump(),
|
||||
files=Knowledges.get_file_metadatas_by_id(knowledge.id),
|
||||
files=Knowledges.get_file_metadatas_by_id(knowledge.id, db=db),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
|
@ -356,9 +480,10 @@ async def get_knowledge_files_by_id(
|
|||
direction: Optional[str] = None,
|
||||
page: Optional[int] = 1,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id, db=db)
|
||||
if not knowledge:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -368,7 +493,7 @@ async def get_knowledge_files_by_id(
|
|||
if not (
|
||||
user.role == "admin"
|
||||
or knowledge.user_id == user.id
|
||||
or has_access(user.id, "read", knowledge.access_control)
|
||||
or has_access(user.id, "read", knowledge.access_control, db=db)
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -391,7 +516,7 @@ async def get_knowledge_files_by_id(
|
|||
filter["direction"] = direction
|
||||
|
||||
return Knowledges.search_files_by_id(
|
||||
id, user.id, filter=filter, skip=skip, limit=limit
|
||||
id, user.id, filter=filter, skip=skip, limit=limit, db=db
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -410,8 +535,9 @@ def add_file_to_knowledge_by_id(
|
|||
id: str,
|
||||
form_data: KnowledgeFileIdForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id, db=db)
|
||||
if not knowledge:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -420,7 +546,7 @@ def add_file_to_knowledge_by_id(
|
|||
|
||||
if (
|
||||
knowledge.user_id != user.id
|
||||
and not has_access(user.id, "write", knowledge.access_control)
|
||||
and not has_access(user.id, "write", knowledge.access_control, db=db)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
@ -428,7 +554,7 @@ def add_file_to_knowledge_by_id(
|
|||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
file = Files.get_file_by_id(form_data.file_id)
|
||||
file = Files.get_file_by_id(form_data.file_id, db=db)
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -446,11 +572,12 @@ def add_file_to_knowledge_by_id(
|
|||
request,
|
||||
ProcessFileForm(file_id=form_data.file_id, collection_name=id),
|
||||
user=user,
|
||||
db=db,
|
||||
)
|
||||
|
||||
# Add file to knowledge base
|
||||
Knowledges.add_file_to_knowledge_by_id(
|
||||
knowledge_id=id, file_id=form_data.file_id, user_id=user.id
|
||||
knowledge_id=id, file_id=form_data.file_id, user_id=user.id, db=db
|
||||
)
|
||||
except Exception as e:
|
||||
log.debug(e)
|
||||
|
|
@ -462,7 +589,7 @@ def add_file_to_knowledge_by_id(
|
|||
if knowledge:
|
||||
return KnowledgeFilesResponse(
|
||||
**knowledge.model_dump(),
|
||||
files=Knowledges.get_file_metadatas_by_id(knowledge.id),
|
||||
files=Knowledges.get_file_metadatas_by_id(knowledge.id, db=db),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
|
@ -477,8 +604,9 @@ def update_file_from_knowledge_by_id(
|
|||
id: str,
|
||||
form_data: KnowledgeFileIdForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id, db=db)
|
||||
if not knowledge:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -487,7 +615,7 @@ def update_file_from_knowledge_by_id(
|
|||
|
||||
if (
|
||||
knowledge.user_id != user.id
|
||||
and not has_access(user.id, "write", knowledge.access_control)
|
||||
and not has_access(user.id, "write", knowledge.access_control, db=db)
|
||||
and user.role != "admin"
|
||||
):
|
||||
|
||||
|
|
@ -496,7 +624,7 @@ def update_file_from_knowledge_by_id(
|
|||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
file = Files.get_file_by_id(form_data.file_id)
|
||||
file = Files.get_file_by_id(form_data.file_id, db=db)
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -514,6 +642,7 @@ def update_file_from_knowledge_by_id(
|
|||
request,
|
||||
ProcessFileForm(file_id=form_data.file_id, collection_name=id),
|
||||
user=user,
|
||||
db=db,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
|
|
@ -524,7 +653,7 @@ def update_file_from_knowledge_by_id(
|
|||
if knowledge:
|
||||
return KnowledgeFilesResponse(
|
||||
**knowledge.model_dump(),
|
||||
files=Knowledges.get_file_metadatas_by_id(knowledge.id),
|
||||
files=Knowledges.get_file_metadatas_by_id(knowledge.id, db=db),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
|
@ -544,8 +673,9 @@ def remove_file_from_knowledge_by_id(
|
|||
form_data: KnowledgeFileIdForm,
|
||||
delete_file: bool = Query(True),
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id, db=db)
|
||||
if not knowledge:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -554,7 +684,7 @@ def remove_file_from_knowledge_by_id(
|
|||
|
||||
if (
|
||||
knowledge.user_id != user.id
|
||||
and not has_access(user.id, "write", knowledge.access_control)
|
||||
and not has_access(user.id, "write", knowledge.access_control, db=db)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
@ -562,7 +692,7 @@ def remove_file_from_knowledge_by_id(
|
|||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
file = Files.get_file_by_id(form_data.file_id)
|
||||
file = Files.get_file_by_id(form_data.file_id, db=db)
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -570,7 +700,7 @@ def remove_file_from_knowledge_by_id(
|
|||
)
|
||||
|
||||
Knowledges.remove_file_from_knowledge_by_id(
|
||||
knowledge_id=id, file_id=form_data.file_id
|
||||
knowledge_id=id, file_id=form_data.file_id, db=db
|
||||
)
|
||||
|
||||
# Remove content from the vector database
|
||||
|
|
@ -599,12 +729,12 @@ def remove_file_from_knowledge_by_id(
|
|||
pass
|
||||
|
||||
# Delete file from database
|
||||
Files.delete_file_by_id(form_data.file_id)
|
||||
Files.delete_file_by_id(form_data.file_id, db=db)
|
||||
|
||||
if knowledge:
|
||||
return KnowledgeFilesResponse(
|
||||
**knowledge.model_dump(),
|
||||
files=Knowledges.get_file_metadatas_by_id(knowledge.id),
|
||||
files=Knowledges.get_file_metadatas_by_id(knowledge.id, db=db),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
|
@ -619,8 +749,10 @@ def remove_file_from_knowledge_by_id(
|
|||
|
||||
|
||||
@router.delete("/{id}/delete", response_model=bool)
|
||||
async def delete_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||
async def delete_knowledge_by_id(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id, db=db)
|
||||
if not knowledge:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -629,7 +761,7 @@ async def delete_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
|||
|
||||
if (
|
||||
knowledge.user_id != user.id
|
||||
and not has_access(user.id, "write", knowledge.access_control)
|
||||
and not has_access(user.id, "write", knowledge.access_control, db=db)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
@ -640,7 +772,7 @@ async def delete_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
|||
log.info(f"Deleting knowledge base: {id} (name: {knowledge.name})")
|
||||
|
||||
# Get all models
|
||||
models = Models.get_all_models()
|
||||
models = Models.get_all_models(db=db)
|
||||
log.info(f"Found {len(models)} models to check for knowledge base {id}")
|
||||
|
||||
# Update models that reference this knowledge base
|
||||
|
|
@ -664,7 +796,7 @@ async def delete_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
|||
access_control=model.access_control,
|
||||
is_active=model.is_active,
|
||||
)
|
||||
Models.update_model_by_id(model.id, model_form)
|
||||
Models.update_model_by_id(model.id, model_form, db=db)
|
||||
|
||||
# Clean up vector DB
|
||||
try:
|
||||
|
|
@ -672,7 +804,11 @@ async def delete_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
|||
except Exception as e:
|
||||
log.debug(e)
|
||||
pass
|
||||
result = Knowledges.delete_knowledge_by_id(id=id)
|
||||
|
||||
# Remove knowledge base embedding
|
||||
remove_knowledge_base_metadata_embedding(id)
|
||||
|
||||
result = Knowledges.delete_knowledge_by_id(id=id, db=db)
|
||||
return result
|
||||
|
||||
|
||||
|
|
@ -682,8 +818,10 @@ async def delete_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
|||
|
||||
|
||||
@router.post("/{id}/reset", response_model=Optional[KnowledgeResponse])
|
||||
async def reset_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||
async def reset_knowledge_by_id(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id, db=db)
|
||||
if not knowledge:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -692,7 +830,7 @@ async def reset_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
|||
|
||||
if (
|
||||
knowledge.user_id != user.id
|
||||
and not has_access(user.id, "write", knowledge.access_control)
|
||||
and not has_access(user.id, "write", knowledge.access_control, db=db)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
@ -706,7 +844,7 @@ async def reset_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
|||
log.debug(e)
|
||||
pass
|
||||
|
||||
knowledge = Knowledges.reset_knowledge_by_id(id=id)
|
||||
knowledge = Knowledges.reset_knowledge_by_id(id=id, db=db)
|
||||
return knowledge
|
||||
|
||||
|
||||
|
|
@ -721,11 +859,12 @@ async def add_files_to_knowledge_batch(
|
|||
id: str,
|
||||
form_data: list[KnowledgeFileIdForm],
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""
|
||||
Add multiple files to a knowledge base
|
||||
"""
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id, db=db)
|
||||
if not knowledge:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -734,7 +873,7 @@ async def add_files_to_knowledge_batch(
|
|||
|
||||
if (
|
||||
knowledge.user_id != user.id
|
||||
and not has_access(user.id, "write", knowledge.access_control)
|
||||
and not has_access(user.id, "write", knowledge.access_control, db=db)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
@ -746,7 +885,7 @@ async def add_files_to_knowledge_batch(
|
|||
log.info(f"files/batch/add - {len(form_data)} files")
|
||||
files: List[FileModel] = []
|
||||
for form in form_data:
|
||||
file = Files.get_file_by_id(form.file_id)
|
||||
file = Files.get_file_by_id(form.file_id, db=db)
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -760,6 +899,7 @@ async def add_files_to_knowledge_batch(
|
|||
request=request,
|
||||
form_data=BatchProcessFilesForm(files=files, collection_name=id),
|
||||
user=user,
|
||||
db=db,
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(
|
||||
|
|
@ -771,7 +911,7 @@ async def add_files_to_knowledge_batch(
|
|||
successful_file_ids = [r.file_id for r in result.results if r.status == "completed"]
|
||||
for file_id in successful_file_ids:
|
||||
Knowledges.add_file_to_knowledge_by_id(
|
||||
knowledge_id=id, file_id=file_id, user_id=user.id
|
||||
knowledge_id=id, file_id=file_id, user_id=user.id, db=db
|
||||
)
|
||||
|
||||
# If there were any errors, include them in the response
|
||||
|
|
@ -779,7 +919,7 @@ async def add_files_to_knowledge_batch(
|
|||
error_details = [f"{err.file_id}: {err.error}" for err in result.errors]
|
||||
return KnowledgeFilesResponse(
|
||||
**knowledge.model_dump(),
|
||||
files=Knowledges.get_file_metadatas_by_id(knowledge.id),
|
||||
files=Knowledges.get_file_metadatas_by_id(knowledge.id, db=db),
|
||||
warnings={
|
||||
"message": "Some files failed to process",
|
||||
"errors": error_details,
|
||||
|
|
@ -788,5 +928,53 @@ async def add_files_to_knowledge_batch(
|
|||
|
||||
return KnowledgeFilesResponse(
|
||||
**knowledge.model_dump(),
|
||||
files=Knowledges.get_file_metadatas_by_id(knowledge.id),
|
||||
files=Knowledges.get_file_metadatas_by_id(knowledge.id, db=db),
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# ExportKnowledgeById
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/{id}/export")
|
||||
async def export_knowledge_by_id(
|
||||
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
"""
|
||||
Export a knowledge base as a zip file containing .txt files.
|
||||
Admin only.
|
||||
"""
|
||||
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id, db=db)
|
||||
if not knowledge:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
files = Knowledges.get_files_by_id(id, db=db)
|
||||
|
||||
# Create zip file in memory
|
||||
zip_buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
for file in files:
|
||||
content = file.data.get("content", "") if file.data else ""
|
||||
if content:
|
||||
# Use original filename with .txt extension
|
||||
filename = file.filename
|
||||
if not filename.endswith(".txt"):
|
||||
filename = f"{filename}.txt"
|
||||
zf.writestr(filename, content)
|
||||
|
||||
zip_buffer.seek(0)
|
||||
|
||||
# Sanitize knowledge name for filename
|
||||
safe_name = "".join(c if c.isalnum() or c in " -_" else "_" for c in knowledge.name)
|
||||
zip_filename = f"{safe_name}.zip"
|
||||
|
||||
return StreamingResponse(
|
||||
zip_buffer,
|
||||
media_type="application/zip",
|
||||
headers={"Content-Disposition": f"attachment; filename={zip_filename}"},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from pydantic import BaseModel
|
||||
import logging
|
||||
import asyncio
|
||||
|
|
@ -7,7 +7,11 @@ from typing import Optional
|
|||
from open_webui.models.memories import Memories, MemoryModel
|
||||
from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
|
||||
from open_webui.utils.auth import get_verified_user
|
||||
from open_webui.internal.db import get_session
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from open_webui.utils.access_control import has_permission
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -25,8 +29,26 @@ async def get_embeddings(request: Request):
|
|||
|
||||
|
||||
@router.get("/", response_model=list[MemoryModel])
|
||||
async def get_memories(user=Depends(get_verified_user)):
|
||||
return Memories.get_memories_by_user_id(user.id)
|
||||
async def get_memories(
|
||||
request: Request,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if not request.app.state.config.ENABLE_MEMORIES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if not has_permission(
|
||||
user.id, "features.memories", request.app.state.config.USER_PERMISSIONS
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
return Memories.get_memories_by_user_id(user.id, db=db)
|
||||
|
||||
|
||||
############################
|
||||
|
|
@ -47,8 +69,23 @@ async def add_memory(
|
|||
request: Request,
|
||||
form_data: AddMemoryForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
memory = Memories.insert_new_memory(user.id, form_data.content)
|
||||
if not request.app.state.config.ENABLE_MEMORIES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if not has_permission(
|
||||
user.id, "features.memories", request.app.state.config.USER_PERMISSIONS
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
memory = Memories.insert_new_memory(user.id, form_data.content, db=db)
|
||||
|
||||
vector = await request.app.state.EMBEDDING_FUNCTION(memory.content, user=user)
|
||||
|
||||
|
|
@ -79,9 +116,26 @@ class QueryMemoryForm(BaseModel):
|
|||
|
||||
@router.post("/query")
|
||||
async def query_memory(
|
||||
request: Request, form_data: QueryMemoryForm, user=Depends(get_verified_user)
|
||||
request: Request,
|
||||
form_data: QueryMemoryForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
memories = Memories.get_memories_by_user_id(user.id)
|
||||
if not request.app.state.config.ENABLE_MEMORIES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if not has_permission(
|
||||
user.id, "features.memories", request.app.state.config.USER_PERMISSIONS
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
memories = Memories.get_memories_by_user_id(user.id, db=db)
|
||||
if not memories:
|
||||
raise HTTPException(status_code=404, detail="No memories found for user")
|
||||
|
||||
|
|
@ -101,11 +155,27 @@ async def query_memory(
|
|||
############################
|
||||
@router.post("/reset", response_model=bool)
|
||||
async def reset_memory_from_vector_db(
|
||||
request: Request, user=Depends(get_verified_user)
|
||||
request: Request,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if not request.app.state.config.ENABLE_MEMORIES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if not has_permission(
|
||||
user.id, "features.memories", request.app.state.config.USER_PERMISSIONS
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
VECTOR_DB_CLIENT.delete_collection(f"user-memory-{user.id}")
|
||||
|
||||
memories = Memories.get_memories_by_user_id(user.id)
|
||||
memories = Memories.get_memories_by_user_id(user.id, db=db)
|
||||
|
||||
# Generate vectors in parallel
|
||||
vectors = await asyncio.gather(
|
||||
|
|
@ -140,8 +210,26 @@ async def reset_memory_from_vector_db(
|
|||
|
||||
|
||||
@router.delete("/delete/user", response_model=bool)
|
||||
async def delete_memory_by_user_id(user=Depends(get_verified_user)):
|
||||
result = Memories.delete_memories_by_user_id(user.id)
|
||||
async def delete_memory_by_user_id(
|
||||
request: Request,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if not request.app.state.config.ENABLE_MEMORIES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if not has_permission(
|
||||
user.id, "features.memories", request.app.state.config.USER_PERMISSIONS
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
result = Memories.delete_memories_by_user_id(user.id, db=db)
|
||||
|
||||
if result:
|
||||
try:
|
||||
|
|
@ -164,9 +252,24 @@ async def update_memory_by_id(
|
|||
request: Request,
|
||||
form_data: MemoryUpdateModel,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if not request.app.state.config.ENABLE_MEMORIES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if not has_permission(
|
||||
user.id, "features.memories", request.app.state.config.USER_PERMISSIONS
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
memory = Memories.update_memory_by_id_and_user_id(
|
||||
memory_id, user.id, form_data.content
|
||||
memory_id, user.id, form_data.content, db=db
|
||||
)
|
||||
if memory is None:
|
||||
raise HTTPException(status_code=404, detail="Memory not found")
|
||||
|
|
@ -198,8 +301,27 @@ async def update_memory_by_id(
|
|||
|
||||
|
||||
@router.delete("/{memory_id}", response_model=bool)
|
||||
async def delete_memory_by_id(memory_id: str, user=Depends(get_verified_user)):
|
||||
result = Memories.delete_memory_by_id_and_user_id(memory_id, user.id)
|
||||
async def delete_memory_by_id(
|
||||
memory_id: str,
|
||||
request: Request,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if not request.app.state.config.ENABLE_MEMORIES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if not has_permission(
|
||||
user.id, "features.memories", request.app.state.config.USER_PERMISSIONS
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
result = Memories.delete_memory_by_id_and_user_id(memory_id, user.id, db=db)
|
||||
|
||||
if result:
|
||||
VECTOR_DB_CLIENT.delete(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ from open_webui.models.models import (
|
|||
ModelModel,
|
||||
ModelResponse,
|
||||
ModelListResponse,
|
||||
ModelAccessListResponse,
|
||||
ModelAccessResponse,
|
||||
Models,
|
||||
)
|
||||
|
||||
|
|
@ -30,6 +32,8 @@ from fastapi.responses import FileResponse, StreamingResponse
|
|||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from open_webui.utils.access_control import has_access, has_permission
|
||||
from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL, STATIC_DIR
|
||||
from open_webui.internal.db import get_session
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -49,7 +53,7 @@ PAGE_ITEM_COUNT = 30
|
|||
|
||||
|
||||
@router.get(
|
||||
"/list", response_model=ModelListResponse
|
||||
"/list", response_model=ModelAccessListResponse
|
||||
) # do NOT use "/" as path, conflicts with main.py
|
||||
async def get_models(
|
||||
query: Optional[str] = None,
|
||||
|
|
@ -59,6 +63,7 @@ async def get_models(
|
|||
direction: Optional[str] = None,
|
||||
page: Optional[int] = 1,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
|
||||
limit = PAGE_ITEM_COUNT
|
||||
|
|
@ -79,13 +84,27 @@ async def get_models(
|
|||
filter["direction"] = direction
|
||||
|
||||
if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL:
|
||||
groups = Groups.get_groups_by_member_id(user.id)
|
||||
groups = Groups.get_groups_by_member_id(user.id, db=db)
|
||||
if groups:
|
||||
filter["group_ids"] = [group.id for group in groups]
|
||||
|
||||
filter["user_id"] = user.id
|
||||
|
||||
return Models.search_models(user.id, filter=filter, skip=skip, limit=limit)
|
||||
result = Models.search_models(user.id, filter=filter, skip=skip, limit=limit, db=db)
|
||||
return ModelAccessListResponse(
|
||||
items=[
|
||||
ModelAccessResponse(
|
||||
**model.model_dump(),
|
||||
write_access=(
|
||||
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
or user.id == model.user_id
|
||||
or has_access(user.id, "write", model.access_control, db=db)
|
||||
),
|
||||
)
|
||||
for model in result.items
|
||||
],
|
||||
total=result.total,
|
||||
)
|
||||
|
||||
|
||||
###########################
|
||||
|
|
@ -94,8 +113,10 @@ async def get_models(
|
|||
|
||||
|
||||
@router.get("/base", response_model=list[ModelResponse])
|
||||
async def get_base_models(user=Depends(get_admin_user)):
|
||||
return Models.get_base_models()
|
||||
async def get_base_models(
|
||||
user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
return Models.get_base_models(db=db)
|
||||
|
||||
|
||||
###########################
|
||||
|
|
@ -104,11 +125,13 @@ async def get_base_models(user=Depends(get_admin_user)):
|
|||
|
||||
|
||||
@router.get("/tags", response_model=list[str])
|
||||
async def get_model_tags(user=Depends(get_verified_user)):
|
||||
async def get_model_tags(
|
||||
user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
|
||||
models = Models.get_models()
|
||||
models = Models.get_models(db=db)
|
||||
else:
|
||||
models = Models.get_models_by_user_id(user.id)
|
||||
models = Models.get_models_by_user_id(user.id, db=db)
|
||||
|
||||
tags_set = set()
|
||||
for model in models:
|
||||
|
|
@ -132,16 +155,17 @@ async def create_new_model(
|
|||
request: Request,
|
||||
form_data: ModelForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if user.role != "admin" and not has_permission(
|
||||
user.id, "workspace.models", request.app.state.config.USER_PERMISSIONS
|
||||
user.id, "workspace.models", request.app.state.config.USER_PERMISSIONS, db=db
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||
)
|
||||
|
||||
model = Models.get_model_by_id(form_data.id)
|
||||
model = Models.get_model_by_id(form_data.id, db=db)
|
||||
if model:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -155,7 +179,7 @@ async def create_new_model(
|
|||
)
|
||||
|
||||
else:
|
||||
model = Models.insert_new_model(form_data, user.id)
|
||||
model = Models.insert_new_model(form_data, user.id, db=db)
|
||||
if model:
|
||||
return model
|
||||
else:
|
||||
|
|
@ -171,9 +195,16 @@ async def create_new_model(
|
|||
|
||||
|
||||
@router.get("/export", response_model=list[ModelModel])
|
||||
async def export_models(request: Request, user=Depends(get_verified_user)):
|
||||
async def export_models(
|
||||
request: Request,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if user.role != "admin" and not has_permission(
|
||||
user.id, "workspace.models_export", request.app.state.config.USER_PERMISSIONS
|
||||
user.id,
|
||||
"workspace.models_export",
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
db=db,
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -181,9 +212,9 @@ async def export_models(request: Request, user=Depends(get_verified_user)):
|
|||
)
|
||||
|
||||
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
|
||||
return Models.get_models()
|
||||
return Models.get_models(db=db)
|
||||
else:
|
||||
return Models.get_models_by_user_id(user.id)
|
||||
return Models.get_models_by_user_id(user.id, db=db)
|
||||
|
||||
|
||||
############################
|
||||
|
|
@ -200,9 +231,13 @@ async def import_models(
|
|||
request: Request,
|
||||
user=Depends(get_verified_user),
|
||||
form_data: ModelsImportForm = (...),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if user.role != "admin" and not has_permission(
|
||||
user.id, "workspace.models_import", request.app.state.config.USER_PERMISSIONS
|
||||
user.id,
|
||||
"workspace.models_import",
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
db=db,
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -216,7 +251,7 @@ async def import_models(
|
|||
model_id = model_data.get("id")
|
||||
|
||||
if model_id and is_valid_model_id(model_id):
|
||||
existing_model = Models.get_model_by_id(model_id)
|
||||
existing_model = Models.get_model_by_id(model_id, db=db)
|
||||
if existing_model:
|
||||
# Update existing model
|
||||
model_data["meta"] = model_data.get("meta", {})
|
||||
|
|
@ -225,13 +260,15 @@ async def import_models(
|
|||
updated_model = ModelForm(
|
||||
**{**existing_model.model_dump(), **model_data}
|
||||
)
|
||||
Models.update_model_by_id(model_id, updated_model)
|
||||
Models.update_model_by_id(model_id, updated_model, db=db)
|
||||
else:
|
||||
# Insert new model
|
||||
model_data["meta"] = model_data.get("meta", {})
|
||||
model_data["params"] = model_data.get("params", {})
|
||||
new_model = ModelForm(**model_data)
|
||||
Models.insert_new_model(user_id=user.id, form_data=new_model)
|
||||
Models.insert_new_model(
|
||||
user_id=user.id, form_data=new_model, db=db
|
||||
)
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Invalid JSON format")
|
||||
|
|
@ -251,9 +288,12 @@ class SyncModelsForm(BaseModel):
|
|||
|
||||
@router.post("/sync", response_model=list[ModelModel])
|
||||
async def sync_models(
|
||||
request: Request, form_data: SyncModelsForm, user=Depends(get_admin_user)
|
||||
request: Request,
|
||||
form_data: SyncModelsForm,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
return Models.sync_models(user.id, form_data.models)
|
||||
return Models.sync_models(user.id, form_data.models, db=db)
|
||||
|
||||
|
||||
###########################
|
||||
|
|
@ -266,19 +306,33 @@ class ModelIdForm(BaseModel):
|
|||
|
||||
|
||||
# Note: We're not using the typical url path param here, but instead using a query parameter to allow '/' in the id
|
||||
@router.get("/model", response_model=Optional[ModelResponse])
|
||||
async def get_model_by_id(id: str, user=Depends(get_verified_user)):
|
||||
model = Models.get_model_by_id(id)
|
||||
@router.get("/model", response_model=Optional[ModelAccessResponse])
|
||||
async def get_model_by_id(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
model = Models.get_model_by_id(id, db=db)
|
||||
if model:
|
||||
if (
|
||||
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
or model.user_id == user.id
|
||||
or has_access(user.id, "read", model.access_control)
|
||||
or has_access(user.id, "read", model.access_control, db=db)
|
||||
):
|
||||
return model
|
||||
return ModelAccessResponse(
|
||||
**model.model_dump(),
|
||||
write_access=(
|
||||
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
or user.id == model.user_id
|
||||
or has_access(user.id, "write", model.access_control, db=db)
|
||||
),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
|
@ -289,38 +343,42 @@ async def get_model_by_id(id: str, user=Depends(get_verified_user)):
|
|||
|
||||
|
||||
@router.get("/model/profile/image")
|
||||
async def get_model_profile_image(id: str, user=Depends(get_verified_user)):
|
||||
model = Models.get_model_by_id(id)
|
||||
# Cache-control headers to prevent stale cached images
|
||||
cache_headers = {"Cache-Control": "no-cache, must-revalidate"}
|
||||
def get_model_profile_image(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
model = Models.get_model_by_id(id, db=db)
|
||||
|
||||
if model:
|
||||
etag = f'"{model.updated_at}"' if model.updated_at else None
|
||||
|
||||
if model.meta.profile_image_url:
|
||||
if model.meta.profile_image_url.startswith("http"):
|
||||
return Response(
|
||||
status_code=status.HTTP_302_FOUND,
|
||||
headers={"Location": model.meta.profile_image_url, **cache_headers},
|
||||
headers={"Location": model.meta.profile_image_url},
|
||||
)
|
||||
elif model.meta.profile_image_url.startswith("data:image"):
|
||||
try:
|
||||
header, base64_data = model.meta.profile_image_url.split(",", 1)
|
||||
image_data = base64.b64decode(base64_data)
|
||||
image_buffer = io.BytesIO(image_data)
|
||||
media_type = header.split(";")[0].lstrip("data:")
|
||||
|
||||
headers = {"Content-Disposition": "inline"}
|
||||
if etag:
|
||||
headers["ETag"] = etag
|
||||
|
||||
return StreamingResponse(
|
||||
image_buffer,
|
||||
media_type="image/png",
|
||||
headers={
|
||||
"Content-Disposition": "inline; filename=image.png",
|
||||
**cache_headers,
|
||||
},
|
||||
media_type=media_type,
|
||||
headers=headers,
|
||||
)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return FileResponse(f"{STATIC_DIR}/favicon.png", headers=cache_headers)
|
||||
return FileResponse(f"{STATIC_DIR}/favicon.png")
|
||||
else:
|
||||
return FileResponse(f"{STATIC_DIR}/favicon.png", headers=cache_headers)
|
||||
return FileResponse(f"{STATIC_DIR}/favicon.png")
|
||||
|
||||
|
||||
############################
|
||||
|
|
@ -329,15 +387,17 @@ async def get_model_profile_image(id: str, user=Depends(get_verified_user)):
|
|||
|
||||
|
||||
@router.post("/model/toggle", response_model=Optional[ModelResponse])
|
||||
async def toggle_model_by_id(id: str, user=Depends(get_verified_user)):
|
||||
model = Models.get_model_by_id(id)
|
||||
async def toggle_model_by_id(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
model = Models.get_model_by_id(id, db=db)
|
||||
if model:
|
||||
if (
|
||||
user.role == "admin"
|
||||
or model.user_id == user.id
|
||||
or has_access(user.id, "write", model.access_control)
|
||||
or has_access(user.id, "write", model.access_control, db=db)
|
||||
):
|
||||
model = Models.toggle_model_by_id(id)
|
||||
model = Models.toggle_model_by_id(id, db=db)
|
||||
|
||||
if model:
|
||||
return model
|
||||
|
|
@ -367,8 +427,9 @@ async def toggle_model_by_id(id: str, user=Depends(get_verified_user)):
|
|||
async def update_model_by_id(
|
||||
form_data: ModelForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
model = Models.get_model_by_id(form_data.id)
|
||||
model = Models.get_model_by_id(form_data.id, db=db)
|
||||
if not model:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -377,7 +438,7 @@ async def update_model_by_id(
|
|||
|
||||
if (
|
||||
model.user_id != user.id
|
||||
and not has_access(user.id, "write", model.access_control)
|
||||
and not has_access(user.id, "write", model.access_control, db=db)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
@ -385,7 +446,9 @@ async def update_model_by_id(
|
|||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
model = Models.update_model_by_id(form_data.id, ModelForm(**form_data.model_dump()))
|
||||
model = Models.update_model_by_id(
|
||||
form_data.id, ModelForm(**form_data.model_dump()), db=db
|
||||
)
|
||||
return model
|
||||
|
||||
|
||||
|
|
@ -395,8 +458,12 @@ async def update_model_by_id(
|
|||
|
||||
|
||||
@router.post("/model/delete", response_model=bool)
|
||||
async def delete_model_by_id(form_data: ModelIdForm, user=Depends(get_verified_user)):
|
||||
model = Models.get_model_by_id(form_data.id)
|
||||
async def delete_model_by_id(
|
||||
form_data: ModelIdForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
model = Models.get_model_by_id(form_data.id, db=db)
|
||||
if not model:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -406,18 +473,20 @@ async def delete_model_by_id(form_data: ModelIdForm, user=Depends(get_verified_u
|
|||
if (
|
||||
user.role != "admin"
|
||||
and model.user_id != user.id
|
||||
and not has_access(user.id, "write", model.access_control)
|
||||
and not has_access(user.id, "write", model.access_control, db=db)
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||
)
|
||||
|
||||
result = Models.delete_model_by_id(form_data.id)
|
||||
result = Models.delete_model_by_id(form_data.id, db=db)
|
||||
return result
|
||||
|
||||
|
||||
@router.delete("/delete/all", response_model=bool)
|
||||
async def delete_all_models(user=Depends(get_admin_user)):
|
||||
result = Models.delete_all_models()
|
||||
async def delete_all_models(
|
||||
user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
result = Models.delete_all_models(db=db)
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ from open_webui.constants import ERROR_MESSAGES
|
|||
|
||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from open_webui.utils.access_control import has_access, has_permission
|
||||
from open_webui.internal.db import get_session
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -49,10 +51,13 @@ class NoteItemResponse(BaseModel):
|
|||
|
||||
@router.get("/", response_model=list[NoteItemResponse])
|
||||
async def get_notes(
|
||||
request: Request, page: Optional[int] = None, user=Depends(get_verified_user)
|
||||
request: Request,
|
||||
page: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if user.role != "admin" and not has_permission(
|
||||
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
|
||||
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -65,16 +70,23 @@ async def get_notes(
|
|||
limit = 60
|
||||
skip = (page - 1) * limit
|
||||
|
||||
notes = [
|
||||
notes = Notes.get_notes_by_user_id(user.id, "read", skip=skip, limit=limit, db=db)
|
||||
if not notes:
|
||||
return []
|
||||
|
||||
user_ids = list(set(note.user_id for note in notes))
|
||||
users = {user.id: user for user in Users.get_users_by_user_ids(user_ids, db=db)}
|
||||
|
||||
return [
|
||||
NoteUserResponse(
|
||||
**{
|
||||
**note.model_dump(),
|
||||
"user": UserResponse(**Users.get_user_by_id(note.user_id).model_dump()),
|
||||
"user": UserResponse(**users[note.user_id].model_dump()),
|
||||
}
|
||||
)
|
||||
for note in Notes.get_notes_by_user_id(user.id, "read", skip=skip, limit=limit)
|
||||
for note in notes
|
||||
if note.user_id in users
|
||||
]
|
||||
return notes
|
||||
|
||||
|
||||
@router.get("/search", response_model=NoteListResponse)
|
||||
|
|
@ -87,9 +99,10 @@ async def search_notes(
|
|||
direction: Optional[str] = None,
|
||||
page: Optional[int] = 1,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if user.role != "admin" and not has_permission(
|
||||
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
|
||||
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -115,13 +128,13 @@ async def search_notes(
|
|||
filter["direction"] = direction
|
||||
|
||||
if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL:
|
||||
groups = Groups.get_groups_by_member_id(user.id)
|
||||
groups = Groups.get_groups_by_member_id(user.id, db=db)
|
||||
if groups:
|
||||
filter["group_ids"] = [group.id for group in groups]
|
||||
|
||||
filter["user_id"] = user.id
|
||||
|
||||
return Notes.search_notes(user.id, filter, skip=skip, limit=limit)
|
||||
return Notes.search_notes(user.id, filter, skip=skip, limit=limit, db=db)
|
||||
|
||||
|
||||
############################
|
||||
|
|
@ -131,10 +144,13 @@ async def search_notes(
|
|||
|
||||
@router.post("/create", response_model=Optional[NoteModel])
|
||||
async def create_new_note(
|
||||
request: Request, form_data: NoteForm, user=Depends(get_verified_user)
|
||||
request: Request,
|
||||
form_data: NoteForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if user.role != "admin" and not has_permission(
|
||||
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
|
||||
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -142,7 +158,7 @@ async def create_new_note(
|
|||
)
|
||||
|
||||
try:
|
||||
note = Notes.insert_new_note(form_data, user.id)
|
||||
note = Notes.insert_new_note(user.id, form_data, db=db)
|
||||
return note
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
|
@ -161,16 +177,21 @@ class NoteResponse(NoteModel):
|
|||
|
||||
|
||||
@router.get("/{id}", response_model=Optional[NoteResponse])
|
||||
async def get_note_by_id(request: Request, id: str, user=Depends(get_verified_user)):
|
||||
async def get_note_by_id(
|
||||
request: Request,
|
||||
id: str,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if user.role != "admin" and not has_permission(
|
||||
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
|
||||
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||
)
|
||||
|
||||
note = Notes.get_note_by_id(id)
|
||||
note = Notes.get_note_by_id(id, db=db)
|
||||
if not note:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||
|
|
@ -178,7 +199,11 @@ async def get_note_by_id(request: Request, id: str, user=Depends(get_verified_us
|
|||
|
||||
if user.role != "admin" and (
|
||||
user.id != note.user_id
|
||||
and (not has_access(user.id, type="read", access_control=note.access_control))
|
||||
and (
|
||||
not has_access(
|
||||
user.id, type="read", access_control=note.access_control, db=db
|
||||
)
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||
|
|
@ -188,7 +213,11 @@ async def get_note_by_id(request: Request, id: str, user=Depends(get_verified_us
|
|||
user.role == "admin"
|
||||
or (user.id == note.user_id)
|
||||
or has_access(
|
||||
user.id, type="write", access_control=note.access_control, strict=False
|
||||
user.id,
|
||||
type="write",
|
||||
access_control=note.access_control,
|
||||
strict=False,
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -202,17 +231,21 @@ async def get_note_by_id(request: Request, id: str, user=Depends(get_verified_us
|
|||
|
||||
@router.post("/{id}/update", response_model=Optional[NoteModel])
|
||||
async def update_note_by_id(
|
||||
request: Request, id: str, form_data: NoteForm, user=Depends(get_verified_user)
|
||||
request: Request,
|
||||
id: str,
|
||||
form_data: NoteForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if user.role != "admin" and not has_permission(
|
||||
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
|
||||
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||
)
|
||||
|
||||
note = Notes.get_note_by_id(id)
|
||||
note = Notes.get_note_by_id(id, db=db)
|
||||
if not note:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||
|
|
@ -220,7 +253,9 @@ async def update_note_by_id(
|
|||
|
||||
if user.role != "admin" and (
|
||||
user.id != note.user_id
|
||||
and not has_access(user.id, type="write", access_control=note.access_control)
|
||||
and not has_access(
|
||||
user.id, type="write", access_control=note.access_control, db=db
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||
|
|
@ -234,12 +269,13 @@ async def update_note_by_id(
|
|||
user.id,
|
||||
"sharing.public_notes",
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
db=db,
|
||||
)
|
||||
):
|
||||
form_data.access_control = {}
|
||||
|
||||
try:
|
||||
note = Notes.update_note_by_id(id, form_data)
|
||||
note = Notes.update_note_by_id(id, form_data, db=db)
|
||||
await sio.emit(
|
||||
"note-events",
|
||||
note.model_dump(),
|
||||
|
|
@ -260,16 +296,21 @@ async def update_note_by_id(
|
|||
|
||||
|
||||
@router.delete("/{id}/delete", response_model=bool)
|
||||
async def delete_note_by_id(request: Request, id: str, user=Depends(get_verified_user)):
|
||||
async def delete_note_by_id(
|
||||
request: Request,
|
||||
id: str,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if user.role != "admin" and not has_permission(
|
||||
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
|
||||
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||
)
|
||||
|
||||
note = Notes.get_note_by_id(id)
|
||||
note = Notes.get_note_by_id(id, db=db)
|
||||
if not note:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||
|
|
@ -277,14 +318,16 @@ async def delete_note_by_id(request: Request, id: str, user=Depends(get_verified
|
|||
|
||||
if user.role != "admin" and (
|
||||
user.id != note.user_id
|
||||
and not has_access(user.id, type="write", access_control=note.access_control)
|
||||
and not has_access(
|
||||
user.id, type="write", access_control=note.access_control, db=db
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||
)
|
||||
|
||||
try:
|
||||
note = Notes.delete_note_by_id(id)
|
||||
note = Notes.delete_note_by_id(id, db=db)
|
||||
return True
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel, ConfigDict, validator
|
||||
from starlette.background import BackgroundTask
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from open_webui.internal.db import get_session
|
||||
|
||||
|
||||
from open_webui.models.models import Models
|
||||
|
|
@ -421,14 +424,14 @@ async def get_all_models(request: Request, user: UserModel = None):
|
|||
return models
|
||||
|
||||
|
||||
async def get_filtered_models(models, user):
|
||||
async def get_filtered_models(models, user, db=None):
|
||||
# Filter models based on user access control
|
||||
filtered_models = []
|
||||
for model in models.get("models", []):
|
||||
model_info = Models.get_model_by_id(model["model"])
|
||||
model_info = Models.get_model_by_id(model["model"], db=db)
|
||||
if model_info:
|
||||
if user.id == model_info.user_id or has_access(
|
||||
user.id, type="read", access_control=model_info.access_control
|
||||
user.id, type="read", access_control=model_info.access_control, db=db
|
||||
):
|
||||
filtered_models.append(model)
|
||||
return filtered_models
|
||||
|
|
@ -1253,6 +1256,8 @@ async def generate_chat_completion(
|
|||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
bypass_filter: Optional[bool] = False,
|
||||
bypass_system_prompt: bool = False,
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if BYPASS_MODEL_ACCESS_CONTROL:
|
||||
bypass_filter = True
|
||||
|
|
@ -1274,7 +1279,7 @@ async def generate_chat_completion(
|
|||
del payload["metadata"]
|
||||
|
||||
model_id = payload["model"]
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
model_info = Models.get_model_by_id(model_id, db=db)
|
||||
|
||||
if model_info:
|
||||
if model_info.base_model_id:
|
||||
|
|
@ -1291,14 +1296,18 @@ async def generate_chat_completion(
|
|||
system = params.pop("system", None)
|
||||
|
||||
payload = apply_model_params_to_body_ollama(params, payload)
|
||||
payload = apply_system_prompt_to_body(system, payload, metadata, user)
|
||||
if not bypass_system_prompt:
|
||||
payload = apply_system_prompt_to_body(system, payload, metadata, user)
|
||||
|
||||
# Check if user has access to the model
|
||||
if not bypass_filter and user.role == "user":
|
||||
if not (
|
||||
user.id == model_info.user_id
|
||||
or has_access(
|
||||
user.id, type="read", access_control=model_info.access_control
|
||||
user.id,
|
||||
type="read",
|
||||
access_control=model_info.access_control,
|
||||
db=db,
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
@ -1370,6 +1379,7 @@ async def generate_openai_completion(
|
|||
form_data: dict,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
metadata = form_data.pop("metadata", None)
|
||||
|
||||
|
|
@ -1390,7 +1400,7 @@ async def generate_openai_completion(
|
|||
if ":" not in model_id:
|
||||
model_id = f"{model_id}:latest"
|
||||
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
model_info = Models.get_model_by_id(model_id, db=db)
|
||||
if model_info:
|
||||
if model_info.base_model_id:
|
||||
payload["model"] = model_info.base_model_id
|
||||
|
|
@ -1404,7 +1414,10 @@ async def generate_openai_completion(
|
|||
if not (
|
||||
user.id == model_info.user_id
|
||||
or has_access(
|
||||
user.id, type="read", access_control=model_info.access_control
|
||||
user.id,
|
||||
type="read",
|
||||
access_control=model_info.access_control,
|
||||
db=db,
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
@ -1449,6 +1462,7 @@ async def generate_openai_chat_completion(
|
|||
form_data: dict,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
metadata = form_data.pop("metadata", None)
|
||||
|
||||
|
|
@ -1469,7 +1483,7 @@ async def generate_openai_chat_completion(
|
|||
if ":" not in model_id:
|
||||
model_id = f"{model_id}:latest"
|
||||
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
model_info = Models.get_model_by_id(model_id, db=db)
|
||||
if model_info:
|
||||
if model_info.base_model_id:
|
||||
payload["model"] = model_info.base_model_id
|
||||
|
|
@ -1487,7 +1501,10 @@ async def generate_openai_chat_completion(
|
|||
if not (
|
||||
user.id == model_info.user_id
|
||||
or has_access(
|
||||
user.id, type="read", access_control=model_info.access_control
|
||||
user.id,
|
||||
type="read",
|
||||
access_control=model_info.access_control,
|
||||
db=db,
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
@ -1530,6 +1547,7 @@ async def get_openai_models(
|
|||
request: Request,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
|
||||
models = []
|
||||
|
|
@ -1582,10 +1600,13 @@ async def get_openai_models(
|
|||
# Filter models based on user access control
|
||||
filtered_models = []
|
||||
for model in models:
|
||||
model_info = Models.get_model_by_id(model["id"])
|
||||
model_info = Models.get_model_by_id(model["id"], db=db)
|
||||
if model_info:
|
||||
if user.id == model_info.user_id or has_access(
|
||||
user.id, type="read", access_control=model_info.access_control
|
||||
user.id,
|
||||
type="read",
|
||||
access_control=model_info.access_control,
|
||||
db=db,
|
||||
):
|
||||
filtered_models.append(model)
|
||||
models = filtered_models
|
||||
|
|
@ -1652,11 +1673,10 @@ async def download_file_stream(
|
|||
|
||||
if done:
|
||||
file.close()
|
||||
hashed = calculate_sha256(file_path, chunk_size)
|
||||
|
||||
with open(file_path, "rb") as file:
|
||||
chunk_size = 1024 * 1024 * 2
|
||||
hashed = calculate_sha256(file, chunk_size)
|
||||
|
||||
url = f"{ollama_url}/api/blobs/sha256:{hashed}"
|
||||
with requests.Session() as session:
|
||||
response = session.post(url, data=file, timeout=30)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ from fastapi.responses import (
|
|||
)
|
||||
from pydantic import BaseModel
|
||||
from starlette.background import BackgroundTask
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from open_webui.internal.db import get_session
|
||||
|
||||
from open_webui.models.models import Models
|
||||
from open_webui.config import (
|
||||
|
|
@ -453,14 +456,14 @@ async def get_all_models_responses(request: Request, user: UserModel) -> list:
|
|||
return responses
|
||||
|
||||
|
||||
async def get_filtered_models(models, user):
|
||||
async def get_filtered_models(models, user, db=None):
|
||||
# Filter models based on user access control
|
||||
filtered_models = []
|
||||
for model in models.get("data", []):
|
||||
model_info = Models.get_model_by_id(model["id"])
|
||||
model_info = Models.get_model_by_id(model["id"], db=db)
|
||||
if model_info:
|
||||
if user.id == model_info.user_id or has_access(
|
||||
user.id, type="read", access_control=model_info.access_control
|
||||
user.id, type="read", access_control=model_info.access_control, db=db
|
||||
):
|
||||
filtered_models.append(model)
|
||||
return filtered_models
|
||||
|
|
@ -797,6 +800,8 @@ async def generate_chat_completion(
|
|||
form_data: dict,
|
||||
user=Depends(get_verified_user),
|
||||
bypass_filter: Optional[bool] = False,
|
||||
bypass_system_prompt: bool = False,
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if BYPASS_MODEL_ACCESS_CONTROL:
|
||||
bypass_filter = True
|
||||
|
|
@ -807,7 +812,7 @@ async def generate_chat_completion(
|
|||
metadata = payload.pop("metadata", None)
|
||||
|
||||
model_id = form_data.get("model")
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
model_info = Models.get_model_by_id(model_id, db=db)
|
||||
|
||||
# Check model info and override the payload
|
||||
if model_info:
|
||||
|
|
@ -826,14 +831,18 @@ async def generate_chat_completion(
|
|||
system = params.pop("system", None)
|
||||
|
||||
payload = apply_model_params_to_body_openai(params, payload)
|
||||
payload = apply_system_prompt_to_body(system, payload, metadata, user)
|
||||
if not bypass_system_prompt:
|
||||
payload = apply_system_prompt_to_body(system, payload, metadata, user)
|
||||
|
||||
# Check if user has access to the model
|
||||
if not bypass_filter and user.role == "user":
|
||||
if not (
|
||||
user.id == model_info.user_id
|
||||
or has_access(
|
||||
user.id, type="read", access_control=model_info.access_control
|
||||
user.id,
|
||||
type="read",
|
||||
access_control=model_info.access_control,
|
||||
db=db,
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, Request
|
|||
from open_webui.models.prompts import (
|
||||
PromptForm,
|
||||
PromptUserResponse,
|
||||
PromptAccessResponse,
|
||||
PromptModel,
|
||||
Prompts,
|
||||
)
|
||||
|
|
@ -11,6 +12,8 @@ from open_webui.constants import ERROR_MESSAGES
|
|||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from open_webui.utils.access_control import has_access, has_permission
|
||||
from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL
|
||||
from open_webui.internal.db import get_session
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
|
@ -20,23 +23,37 @@ router = APIRouter()
|
|||
|
||||
|
||||
@router.get("/", response_model=list[PromptModel])
|
||||
async def get_prompts(user=Depends(get_verified_user)):
|
||||
async def get_prompts(
|
||||
user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
|
||||
prompts = Prompts.get_prompts()
|
||||
prompts = Prompts.get_prompts(db=db)
|
||||
else:
|
||||
prompts = Prompts.get_prompts_by_user_id(user.id, "read")
|
||||
prompts = Prompts.get_prompts_by_user_id(user.id, "read", db=db)
|
||||
|
||||
return prompts
|
||||
|
||||
|
||||
@router.get("/list", response_model=list[PromptUserResponse])
|
||||
async def get_prompt_list(user=Depends(get_verified_user)):
|
||||
@router.get("/list", response_model=list[PromptAccessResponse])
|
||||
async def get_prompt_list(
|
||||
user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
|
||||
prompts = Prompts.get_prompts()
|
||||
prompts = Prompts.get_prompts(db=db)
|
||||
else:
|
||||
prompts = Prompts.get_prompts_by_user_id(user.id, "write")
|
||||
prompts = Prompts.get_prompts_by_user_id(user.id, "read", db=db)
|
||||
|
||||
return prompts
|
||||
return [
|
||||
PromptAccessResponse(
|
||||
**prompt.model_dump(),
|
||||
write_access=(
|
||||
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
or user.id == prompt.user_id
|
||||
or has_access(user.id, "write", prompt.access_control, db=db)
|
||||
),
|
||||
)
|
||||
for prompt in prompts
|
||||
]
|
||||
|
||||
|
||||
############################
|
||||
|
|
@ -46,16 +63,23 @@ async def get_prompt_list(user=Depends(get_verified_user)):
|
|||
|
||||
@router.post("/create", response_model=Optional[PromptModel])
|
||||
async def create_new_prompt(
|
||||
request: Request, form_data: PromptForm, user=Depends(get_verified_user)
|
||||
request: Request,
|
||||
form_data: PromptForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if user.role != "admin" and not (
|
||||
has_permission(
|
||||
user.id, "workspace.prompts", request.app.state.config.USER_PERMISSIONS
|
||||
user.id,
|
||||
"workspace.prompts",
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
db=db,
|
||||
)
|
||||
or has_permission(
|
||||
user.id,
|
||||
"workspace.prompts_import",
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
db=db,
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
@ -63,9 +87,9 @@ async def create_new_prompt(
|
|||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||
)
|
||||
|
||||
prompt = Prompts.get_prompt_by_command(form_data.command)
|
||||
prompt = Prompts.get_prompt_by_command(form_data.command, db=db)
|
||||
if prompt is None:
|
||||
prompt = Prompts.insert_new_prompt(user.id, form_data)
|
||||
prompt = Prompts.insert_new_prompt(user.id, form_data, db=db)
|
||||
|
||||
if prompt:
|
||||
return prompt
|
||||
|
|
@ -84,17 +108,26 @@ async def create_new_prompt(
|
|||
############################
|
||||
|
||||
|
||||
@router.get("/command/{command}", response_model=Optional[PromptModel])
|
||||
async def get_prompt_by_command(command: str, user=Depends(get_verified_user)):
|
||||
prompt = Prompts.get_prompt_by_command(f"/{command}")
|
||||
@router.get("/command/{command}", response_model=Optional[PromptAccessResponse])
|
||||
async def get_prompt_by_command(
|
||||
command: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
prompt = Prompts.get_prompt_by_command(f"/{command}", db=db)
|
||||
|
||||
if prompt:
|
||||
if (
|
||||
user.role == "admin"
|
||||
or prompt.user_id == user.id
|
||||
or has_access(user.id, "read", prompt.access_control)
|
||||
or has_access(user.id, "read", prompt.access_control, db=db)
|
||||
):
|
||||
return prompt
|
||||
return PromptAccessResponse(
|
||||
**prompt.model_dump(),
|
||||
write_access=(
|
||||
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
or user.id == prompt.user_id
|
||||
or has_access(user.id, "write", prompt.access_control, db=db)
|
||||
),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -112,8 +145,9 @@ async def update_prompt_by_command(
|
|||
command: str,
|
||||
form_data: PromptForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
prompt = Prompts.get_prompt_by_command(f"/{command}")
|
||||
prompt = Prompts.get_prompt_by_command(f"/{command}", db=db)
|
||||
if not prompt:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -123,7 +157,7 @@ async def update_prompt_by_command(
|
|||
# Is the user the original creator, in a group with write access, or an admin
|
||||
if (
|
||||
prompt.user_id != user.id
|
||||
and not has_access(user.id, "write", prompt.access_control)
|
||||
and not has_access(user.id, "write", prompt.access_control, db=db)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
@ -131,7 +165,7 @@ async def update_prompt_by_command(
|
|||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
prompt = Prompts.update_prompt_by_command(f"/{command}", form_data)
|
||||
prompt = Prompts.update_prompt_by_command(f"/{command}", form_data, db=db)
|
||||
if prompt:
|
||||
return prompt
|
||||
else:
|
||||
|
|
@ -147,8 +181,10 @@ async def update_prompt_by_command(
|
|||
|
||||
|
||||
@router.delete("/command/{command}/delete", response_model=bool)
|
||||
async def delete_prompt_by_command(command: str, user=Depends(get_verified_user)):
|
||||
prompt = Prompts.get_prompt_by_command(f"/{command}")
|
||||
async def delete_prompt_by_command(
|
||||
command: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
prompt = Prompts.get_prompt_by_command(f"/{command}", db=db)
|
||||
if not prompt:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -157,7 +193,7 @@ async def delete_prompt_by_command(command: str, user=Depends(get_verified_user)
|
|||
|
||||
if (
|
||||
prompt.user_id != user.id
|
||||
and not has_access(user.id, "write", prompt.access_control)
|
||||
and not has_access(user.id, "write", prompt.access_control, db=db)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
@ -165,5 +201,5 @@ async def delete_prompt_by_command(command: str, user=Depends(get_verified_user)
|
|||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
result = Prompts.delete_prompt_by_command(f"/{command}")
|
||||
result = Prompts.delete_prompt_by_command(f"/{command}", db=db)
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ from langchain_core.documents import Document
|
|||
from open_webui.models.files import FileModel, FileUpdateForm, Files
|
||||
from open_webui.models.knowledge import Knowledges
|
||||
from open_webui.storage.provider import Storage
|
||||
from open_webui.internal.db import get_session
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
|
||||
|
|
@ -91,6 +93,7 @@ from open_webui.utils.misc import (
|
|||
sanitize_text_for_db,
|
||||
)
|
||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from open_webui.utils.access_control import has_permission
|
||||
|
||||
from open_webui.config import (
|
||||
ENV,
|
||||
|
|
@ -110,6 +113,7 @@ from open_webui.env import (
|
|||
SENTENCE_TRANSFORMERS_MODEL_KWARGS,
|
||||
SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND,
|
||||
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS,
|
||||
SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION,
|
||||
)
|
||||
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
|
|
@ -188,6 +192,7 @@ def get_rf(
|
|||
raise Exception(ERROR_MESSAGES.DEFAULT(e))
|
||||
else:
|
||||
import sentence_transformers
|
||||
import torch
|
||||
|
||||
try:
|
||||
rf = sentence_transformers.CrossEncoder(
|
||||
|
|
@ -196,6 +201,11 @@ def get_rf(
|
|||
trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
|
||||
backend=SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND,
|
||||
model_kwargs=SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS,
|
||||
activation_fn=(
|
||||
torch.nn.Sigmoid()
|
||||
if SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION
|
||||
else None
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f"CrossEncoder: {e}")
|
||||
|
|
@ -494,7 +504,9 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
|
|||
"RAG_EXTERNAL_RERANKER_TIMEOUT": request.app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT,
|
||||
# Chunking settings
|
||||
"TEXT_SPLITTER": request.app.state.config.TEXT_SPLITTER,
|
||||
"ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER": request.app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER,
|
||||
"CHUNK_SIZE": request.app.state.config.CHUNK_SIZE,
|
||||
"CHUNK_MIN_SIZE_TARGET": request.app.state.config.CHUNK_MIN_SIZE_TARGET,
|
||||
"CHUNK_OVERLAP": request.app.state.config.CHUNK_OVERLAP,
|
||||
# File upload settings
|
||||
"FILE_MAX_SIZE": request.app.state.config.FILE_MAX_SIZE,
|
||||
|
|
@ -532,12 +544,14 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
|
|||
"SERPSTACK_HTTPS": request.app.state.config.SERPSTACK_HTTPS,
|
||||
"SERPER_API_KEY": request.app.state.config.SERPER_API_KEY,
|
||||
"SERPLY_API_KEY": request.app.state.config.SERPLY_API_KEY,
|
||||
"DDGS_BACKEND": request.app.state.config.DDGS_BACKEND,
|
||||
"TAVILY_API_KEY": request.app.state.config.TAVILY_API_KEY,
|
||||
"SEARCHAPI_API_KEY": request.app.state.config.SEARCHAPI_API_KEY,
|
||||
"SEARCHAPI_ENGINE": request.app.state.config.SEARCHAPI_ENGINE,
|
||||
"SERPAPI_API_KEY": request.app.state.config.SERPAPI_API_KEY,
|
||||
"SERPAPI_ENGINE": request.app.state.config.SERPAPI_ENGINE,
|
||||
"JINA_API_KEY": request.app.state.config.JINA_API_KEY,
|
||||
"JINA_API_BASE_URL": request.app.state.config.JINA_API_BASE_URL,
|
||||
"BING_SEARCH_V7_ENDPOINT": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
|
||||
"BING_SEARCH_V7_SUBSCRIPTION_KEY": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
|
||||
"EXA_API_KEY": request.app.state.config.EXA_API_KEY,
|
||||
|
|
@ -554,6 +568,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
|
|||
"PLAYWRIGHT_TIMEOUT": request.app.state.config.PLAYWRIGHT_TIMEOUT,
|
||||
"FIRECRAWL_API_KEY": request.app.state.config.FIRECRAWL_API_KEY,
|
||||
"FIRECRAWL_API_BASE_URL": request.app.state.config.FIRECRAWL_API_BASE_URL,
|
||||
"FIRECRAWL_TIMEOUT": request.app.state.config.FIRECRAWL_TIMEOUT,
|
||||
"TAVILY_EXTRACT_DEPTH": request.app.state.config.TAVILY_EXTRACT_DEPTH,
|
||||
"EXTERNAL_WEB_SEARCH_URL": request.app.state.config.EXTERNAL_WEB_SEARCH_URL,
|
||||
"EXTERNAL_WEB_SEARCH_API_KEY": request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY,
|
||||
|
|
@ -592,12 +607,14 @@ class WebConfig(BaseModel):
|
|||
SERPSTACK_HTTPS: Optional[bool] = None
|
||||
SERPER_API_KEY: Optional[str] = None
|
||||
SERPLY_API_KEY: Optional[str] = None
|
||||
DDGS_BACKEND: Optional[str] = None
|
||||
TAVILY_API_KEY: Optional[str] = None
|
||||
SEARCHAPI_API_KEY: Optional[str] = None
|
||||
SEARCHAPI_ENGINE: Optional[str] = None
|
||||
SERPAPI_API_KEY: Optional[str] = None
|
||||
SERPAPI_ENGINE: Optional[str] = None
|
||||
JINA_API_KEY: Optional[str] = None
|
||||
JINA_API_BASE_URL: Optional[str] = None
|
||||
BING_SEARCH_V7_ENDPOINT: Optional[str] = None
|
||||
BING_SEARCH_V7_SUBSCRIPTION_KEY: Optional[str] = None
|
||||
EXA_API_KEY: Optional[str] = None
|
||||
|
|
@ -614,6 +631,7 @@ class WebConfig(BaseModel):
|
|||
PLAYWRIGHT_TIMEOUT: Optional[int] = None
|
||||
FIRECRAWL_API_KEY: Optional[str] = None
|
||||
FIRECRAWL_API_BASE_URL: Optional[str] = None
|
||||
FIRECRAWL_TIMEOUT: Optional[str] = None
|
||||
TAVILY_EXTRACT_DEPTH: Optional[str] = None
|
||||
EXTERNAL_WEB_SEARCH_URL: Optional[str] = None
|
||||
EXTERNAL_WEB_SEARCH_API_KEY: Optional[str] = None
|
||||
|
|
@ -683,7 +701,9 @@ class ConfigForm(BaseModel):
|
|||
|
||||
# Chunking settings
|
||||
TEXT_SPLITTER: Optional[str] = None
|
||||
ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER: Optional[bool] = None
|
||||
CHUNK_SIZE: Optional[int] = None
|
||||
CHUNK_MIN_SIZE_TARGET: Optional[int] = None
|
||||
CHUNK_OVERLAP: Optional[int] = None
|
||||
|
||||
# File upload settings
|
||||
|
|
@ -991,6 +1011,11 @@ async def update_rag_config(
|
|||
if form_data.CHUNK_SIZE is not None
|
||||
else request.app.state.config.CHUNK_SIZE
|
||||
)
|
||||
request.app.state.config.CHUNK_MIN_SIZE_TARGET = (
|
||||
form_data.CHUNK_MIN_SIZE_TARGET
|
||||
if form_data.CHUNK_MIN_SIZE_TARGET is not None
|
||||
else request.app.state.config.CHUNK_MIN_SIZE_TARGET
|
||||
)
|
||||
request.app.state.config.CHUNK_OVERLAP = (
|
||||
form_data.CHUNK_OVERLAP
|
||||
if form_data.CHUNK_OVERLAP is not None
|
||||
|
|
@ -1075,12 +1100,14 @@ async def update_rag_config(
|
|||
request.app.state.config.SERPSTACK_HTTPS = form_data.web.SERPSTACK_HTTPS
|
||||
request.app.state.config.SERPER_API_KEY = form_data.web.SERPER_API_KEY
|
||||
request.app.state.config.SERPLY_API_KEY = form_data.web.SERPLY_API_KEY
|
||||
request.app.state.config.DDGS_BACKEND = form_data.web.DDGS_BACKEND
|
||||
request.app.state.config.TAVILY_API_KEY = form_data.web.TAVILY_API_KEY
|
||||
request.app.state.config.SEARCHAPI_API_KEY = form_data.web.SEARCHAPI_API_KEY
|
||||
request.app.state.config.SEARCHAPI_ENGINE = form_data.web.SEARCHAPI_ENGINE
|
||||
request.app.state.config.SERPAPI_API_KEY = form_data.web.SERPAPI_API_KEY
|
||||
request.app.state.config.SERPAPI_ENGINE = form_data.web.SERPAPI_ENGINE
|
||||
request.app.state.config.JINA_API_KEY = form_data.web.JINA_API_KEY
|
||||
request.app.state.config.JINA_API_BASE_URL = form_data.web.JINA_API_BASE_URL
|
||||
request.app.state.config.BING_SEARCH_V7_ENDPOINT = (
|
||||
form_data.web.BING_SEARCH_V7_ENDPOINT
|
||||
)
|
||||
|
|
@ -1112,6 +1139,7 @@ async def update_rag_config(
|
|||
request.app.state.config.FIRECRAWL_API_BASE_URL = (
|
||||
form_data.web.FIRECRAWL_API_BASE_URL
|
||||
)
|
||||
request.app.state.config.FIRECRAWL_TIMEOUT = form_data.web.FIRECRAWL_TIMEOUT
|
||||
request.app.state.config.EXTERNAL_WEB_SEARCH_URL = (
|
||||
form_data.web.EXTERNAL_WEB_SEARCH_URL
|
||||
)
|
||||
|
|
@ -1188,6 +1216,8 @@ async def update_rag_config(
|
|||
# Chunking settings
|
||||
"TEXT_SPLITTER": request.app.state.config.TEXT_SPLITTER,
|
||||
"CHUNK_SIZE": request.app.state.config.CHUNK_SIZE,
|
||||
"CHUNK_MIN_SIZE_TARGET": request.app.state.config.CHUNK_MIN_SIZE_TARGET,
|
||||
"ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER": request.app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER,
|
||||
"CHUNK_OVERLAP": request.app.state.config.CHUNK_OVERLAP,
|
||||
# File upload settings
|
||||
"FILE_MAX_SIZE": request.app.state.config.FILE_MAX_SIZE,
|
||||
|
|
@ -1231,6 +1261,7 @@ async def update_rag_config(
|
|||
"SERPAPI_API_KEY": request.app.state.config.SERPAPI_API_KEY,
|
||||
"SERPAPI_ENGINE": request.app.state.config.SERPAPI_ENGINE,
|
||||
"JINA_API_KEY": request.app.state.config.JINA_API_KEY,
|
||||
"JINA_API_BASE_URL": request.app.state.config.JINA_API_BASE_URL,
|
||||
"BING_SEARCH_V7_ENDPOINT": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
|
||||
"BING_SEARCH_V7_SUBSCRIPTION_KEY": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
|
||||
"EXA_API_KEY": request.app.state.config.EXA_API_KEY,
|
||||
|
|
@ -1247,6 +1278,7 @@ async def update_rag_config(
|
|||
"PLAYWRIGHT_TIMEOUT": request.app.state.config.PLAYWRIGHT_TIMEOUT,
|
||||
"FIRECRAWL_API_KEY": request.app.state.config.FIRECRAWL_API_KEY,
|
||||
"FIRECRAWL_API_BASE_URL": request.app.state.config.FIRECRAWL_API_BASE_URL,
|
||||
"FIRECRAWL_TIMEOUT": request.app.state.config.FIRECRAWL_TIMEOUT,
|
||||
"TAVILY_EXTRACT_DEPTH": request.app.state.config.TAVILY_EXTRACT_DEPTH,
|
||||
"EXTERNAL_WEB_SEARCH_URL": request.app.state.config.EXTERNAL_WEB_SEARCH_URL,
|
||||
"EXTERNAL_WEB_SEARCH_API_KEY": request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY,
|
||||
|
|
@ -1266,6 +1298,85 @@ async def update_rag_config(
|
|||
####################################
|
||||
|
||||
|
||||
def can_merge_chunks(a: Document, b: Document) -> bool:
|
||||
if a.metadata.get("source") != b.metadata.get("source"):
|
||||
return False
|
||||
|
||||
a_file_id = a.metadata.get("file_id")
|
||||
b_file_id = b.metadata.get("file_id")
|
||||
|
||||
if a_file_id is not None and b_file_id is not None:
|
||||
return a_file_id == b_file_id
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def merge_docs_to_target_size(
|
||||
request: Request,
|
||||
chunks: list[Document],
|
||||
) -> list[Document]:
|
||||
"""
|
||||
Best-effort normalization of chunk sizes.
|
||||
|
||||
Attempts to grow small chunks up to a desired minimum size,
|
||||
without exceeding the maximum size or crossing source/file
|
||||
boundaries.
|
||||
"""
|
||||
min_chunk_size_target = request.app.state.config.CHUNK_MIN_SIZE_TARGET
|
||||
max_chunk_size = request.app.state.config.CHUNK_SIZE
|
||||
|
||||
if min_chunk_size_target <= 0:
|
||||
return chunks
|
||||
|
||||
measure_chunk_size = len
|
||||
if request.app.state.config.TEXT_SPLITTER == "token":
|
||||
encoding = tiktoken.get_encoding(
|
||||
str(request.app.state.config.TIKTOKEN_ENCODING_NAME)
|
||||
)
|
||||
measure_chunk_size = lambda text: len(encoding.encode(text))
|
||||
|
||||
processed_chunks: list[Document] = []
|
||||
|
||||
current_chunk: Document | None = None
|
||||
current_content: str = ""
|
||||
|
||||
for next_chunk in chunks:
|
||||
if current_chunk is None:
|
||||
current_chunk = next_chunk
|
||||
current_content = next_chunk.page_content
|
||||
continue # First chunk initialization
|
||||
|
||||
proposed_content = f"{current_content}\n\n{next_chunk.page_content}"
|
||||
|
||||
can_merge = (
|
||||
can_merge_chunks(current_chunk, next_chunk)
|
||||
and measure_chunk_size(current_content) < min_chunk_size_target
|
||||
and measure_chunk_size(proposed_content) <= max_chunk_size
|
||||
)
|
||||
|
||||
if can_merge:
|
||||
current_content = proposed_content
|
||||
else:
|
||||
processed_chunks.append(
|
||||
Document(
|
||||
page_content=current_content,
|
||||
metadata={**current_chunk.metadata},
|
||||
)
|
||||
)
|
||||
current_chunk = next_chunk
|
||||
current_content = next_chunk.page_content
|
||||
|
||||
if current_chunk is not None:
|
||||
processed_chunks.append(
|
||||
Document(
|
||||
page_content=current_content,
|
||||
metadata={**current_chunk.metadata},
|
||||
)
|
||||
)
|
||||
|
||||
return processed_chunks
|
||||
|
||||
|
||||
def save_docs_to_vector_db(
|
||||
request: Request,
|
||||
docs,
|
||||
|
|
@ -1303,13 +1414,46 @@ def save_docs_to_vector_db(
|
|||
filter={"hash": metadata["hash"]},
|
||||
)
|
||||
|
||||
if result is not None:
|
||||
if result is not None and result.ids and len(result.ids) > 0:
|
||||
existing_doc_ids = result.ids[0]
|
||||
if existing_doc_ids:
|
||||
log.info(f"Document with hash {metadata['hash']} already exists")
|
||||
raise ValueError(ERROR_MESSAGES.DUPLICATE_CONTENT)
|
||||
|
||||
if split:
|
||||
if request.app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER:
|
||||
log.info("Using markdown header text splitter")
|
||||
# Define headers to split on - covering most common markdown header levels
|
||||
markdown_splitter = MarkdownHeaderTextSplitter(
|
||||
headers_to_split_on=[
|
||||
("#", "Header 1"),
|
||||
("##", "Header 2"),
|
||||
("###", "Header 3"),
|
||||
("####", "Header 4"),
|
||||
("#####", "Header 5"),
|
||||
("######", "Header 6"),
|
||||
],
|
||||
strip_headers=False, # Keep headers in content for context
|
||||
)
|
||||
|
||||
split_docs = []
|
||||
for doc in docs:
|
||||
split_docs.extend(
|
||||
[
|
||||
Document(
|
||||
page_content=split_chunk.page_content,
|
||||
metadata={**doc.metadata},
|
||||
)
|
||||
for split_chunk in markdown_splitter.split_text(
|
||||
doc.page_content
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
docs = split_docs
|
||||
if request.app.state.config.CHUNK_MIN_SIZE_TARGET > 0:
|
||||
docs = merge_docs_to_target_size(request, docs)
|
||||
|
||||
if request.app.state.config.TEXT_SPLITTER in ["", "character"]:
|
||||
text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=request.app.state.config.CHUNK_SIZE,
|
||||
|
|
@ -1330,52 +1474,6 @@ def save_docs_to_vector_db(
|
|||
add_start_index=True,
|
||||
)
|
||||
docs = text_splitter.split_documents(docs)
|
||||
elif request.app.state.config.TEXT_SPLITTER == "markdown_header":
|
||||
log.info("Using markdown header text splitter")
|
||||
|
||||
# Define headers to split on - covering most common markdown header levels
|
||||
headers_to_split_on = [
|
||||
("#", "Header 1"),
|
||||
("##", "Header 2"),
|
||||
("###", "Header 3"),
|
||||
("####", "Header 4"),
|
||||
("#####", "Header 5"),
|
||||
("######", "Header 6"),
|
||||
]
|
||||
|
||||
markdown_splitter = MarkdownHeaderTextSplitter(
|
||||
headers_to_split_on=headers_to_split_on,
|
||||
strip_headers=False, # Keep headers in content for context
|
||||
)
|
||||
|
||||
md_split_docs = []
|
||||
for doc in docs:
|
||||
md_header_splits = markdown_splitter.split_text(doc.page_content)
|
||||
text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=request.app.state.config.CHUNK_SIZE,
|
||||
chunk_overlap=request.app.state.config.CHUNK_OVERLAP,
|
||||
add_start_index=True,
|
||||
)
|
||||
md_header_splits = text_splitter.split_documents(md_header_splits)
|
||||
|
||||
# Convert back to Document objects, preserving original metadata
|
||||
for split_chunk in md_header_splits:
|
||||
headings_list = []
|
||||
# Extract header values in order based on headers_to_split_on
|
||||
for _, header_meta_key_name in headers_to_split_on:
|
||||
if header_meta_key_name in split_chunk.metadata:
|
||||
headings_list.append(
|
||||
split_chunk.metadata[header_meta_key_name]
|
||||
)
|
||||
|
||||
md_split_docs.append(
|
||||
Document(
|
||||
page_content=split_chunk.page_content,
|
||||
metadata={**doc.metadata, "headings": headings_list},
|
||||
)
|
||||
)
|
||||
|
||||
docs = md_split_docs
|
||||
else:
|
||||
raise ValueError(ERROR_MESSAGES.DEFAULT("Invalid text splitter"))
|
||||
|
||||
|
|
@ -1484,14 +1582,15 @@ def process_file(
|
|||
request: Request,
|
||||
form_data: ProcessFileForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""
|
||||
Process a file and save its content to the vector database.
|
||||
"""
|
||||
if user.role == "admin":
|
||||
file = Files.get_file_by_id(form_data.file_id)
|
||||
file = Files.get_file_by_id(form_data.file_id, db=db)
|
||||
else:
|
||||
file = Files.get_file_by_id_and_user_id(form_data.file_id, user.id)
|
||||
file = Files.get_file_by_id_and_user_id(form_data.file_id, user.id, db=db)
|
||||
|
||||
if file:
|
||||
try:
|
||||
|
|
@ -1633,12 +1732,13 @@ def process_file(
|
|||
Files.update_file_data_by_id(
|
||||
file.id,
|
||||
{"content": text_content},
|
||||
db=db,
|
||||
)
|
||||
hash = calculate_sha256_string(text_content)
|
||||
Files.update_file_hash_by_id(file.id, hash)
|
||||
|
||||
if request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL:
|
||||
Files.update_file_data_by_id(file.id, {"status": "completed"})
|
||||
Files.update_file_data_by_id(file.id, {"status": "completed"}, db=db)
|
||||
Files.update_file_hash_by_id(file.id, hash, db=db)
|
||||
return {
|
||||
"status": True,
|
||||
"collection_name": None,
|
||||
|
|
@ -1667,12 +1767,15 @@ def process_file(
|
|||
{
|
||||
"collection_name": collection_name,
|
||||
},
|
||||
db=db,
|
||||
)
|
||||
|
||||
Files.update_file_data_by_id(
|
||||
file.id,
|
||||
{"status": "completed"},
|
||||
db=db,
|
||||
)
|
||||
Files.update_file_hash_by_id(file.id, hash, db=db)
|
||||
|
||||
return {
|
||||
"status": True,
|
||||
|
|
@ -1690,7 +1793,10 @@ def process_file(
|
|||
Files.update_file_data_by_id(
|
||||
file.id,
|
||||
{"status": "failed"},
|
||||
db=db,
|
||||
)
|
||||
# Clear the hash so the file can be re-uploaded after fixing the issue
|
||||
Files.update_file_hash_by_id(file.id, None, db=db)
|
||||
|
||||
if "No pandoc was found" in str(e):
|
||||
raise HTTPException(
|
||||
|
|
@ -1972,6 +2078,7 @@ def search_web(
|
|||
request.app.state.config.WEB_SEARCH_RESULT_COUNT,
|
||||
request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||
concurrent_requests=request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||
backend=request.app.state.config.DDGS_BACKEND,
|
||||
)
|
||||
elif engine == "tavily":
|
||||
if request.app.state.config.TAVILY_API_KEY:
|
||||
|
|
@ -2020,6 +2127,7 @@ def search_web(
|
|||
request.app.state.config.JINA_API_KEY,
|
||||
query,
|
||||
request.app.state.config.WEB_SEARCH_RESULT_COUNT,
|
||||
request.app.state.config.JINA_API_BASE_URL,
|
||||
)
|
||||
elif engine == "bing":
|
||||
return search_bing(
|
||||
|
|
@ -2106,6 +2214,19 @@ def search_web(
|
|||
async def process_web_search(
|
||||
request: Request, form_data: SearchForm, user=Depends(get_verified_user)
|
||||
):
|
||||
if not request.app.state.config.ENABLE_WEB_SEARCH:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
if user.role != "admin" and not has_permission(
|
||||
user.id, "features.web_search", request.app.state.config.USER_PERMISSIONS
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
urls = []
|
||||
result_items = []
|
||||
|
|
@ -2417,10 +2538,19 @@ class DeleteForm(BaseModel):
|
|||
|
||||
|
||||
@router.post("/delete")
|
||||
def delete_entries_from_collection(form_data: DeleteForm, user=Depends(get_admin_user)):
|
||||
def delete_entries_from_collection(
|
||||
form_data: DeleteForm,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
if VECTOR_DB_CLIENT.has_collection(collection_name=form_data.collection_name):
|
||||
file = Files.get_file_by_id(form_data.file_id)
|
||||
file = Files.get_file_by_id(form_data.file_id, db=db)
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
hash = file.hash
|
||||
|
||||
VECTOR_DB_CLIENT.delete(
|
||||
|
|
@ -2436,9 +2566,9 @@ def delete_entries_from_collection(form_data: DeleteForm, user=Depends(get_admin
|
|||
|
||||
|
||||
@router.post("/reset/db")
|
||||
def reset_vector_db(user=Depends(get_admin_user)):
|
||||
def reset_vector_db(user=Depends(get_admin_user), db: Session = Depends(get_session)):
|
||||
VECTOR_DB_CLIENT.reset()
|
||||
Knowledges.delete_all_knowledge()
|
||||
Knowledges.delete_all_knowledge(db=db)
|
||||
|
||||
|
||||
@router.post("/reset/uploads")
|
||||
|
|
@ -2496,6 +2626,7 @@ async def process_files_batch(
|
|||
request: Request,
|
||||
form_data: BatchProcessFilesForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
) -> BatchProcessFilesResponse:
|
||||
"""
|
||||
Process a batch of files and save them to the vector database.
|
||||
|
|
@ -2558,7 +2689,9 @@ async def process_files_batch(
|
|||
|
||||
# Update all files with collection name
|
||||
for file_update, file_result in zip(file_updates, file_results):
|
||||
Files.update_file_by_id(id=file_result.file_id, form_data=file_update)
|
||||
Files.update_file_by_id(
|
||||
id=file_result.file_id, form_data=file_update, db=db
|
||||
)
|
||||
file_result.status = "completed"
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ from open_webui.utils.auth import (
|
|||
)
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import get_session
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
|
@ -296,7 +300,7 @@ def get_scim_auth(
|
|||
)
|
||||
|
||||
|
||||
def user_to_scim(user: UserModel, request: Request) -> SCIMUser:
|
||||
def user_to_scim(user: UserModel, request: Request, db=None) -> SCIMUser:
|
||||
"""Convert internal User model to SCIM User"""
|
||||
# Parse display name into name components
|
||||
name_parts = user.name.split(" ", 1) if user.name else ["", ""]
|
||||
|
|
@ -304,7 +308,7 @@ def user_to_scim(user: UserModel, request: Request) -> SCIMUser:
|
|||
family_name = name_parts[1] if len(name_parts) > 1 else ""
|
||||
|
||||
# Get user's groups
|
||||
user_groups = Groups.get_groups_by_member_id(user.id)
|
||||
user_groups = Groups.get_groups_by_member_id(user.id, db=db)
|
||||
groups = [
|
||||
{
|
||||
"value": group.id,
|
||||
|
|
@ -345,13 +349,13 @@ def user_to_scim(user: UserModel, request: Request) -> SCIMUser:
|
|||
)
|
||||
|
||||
|
||||
def group_to_scim(group: GroupModel, request: Request) -> SCIMGroup:
|
||||
def group_to_scim(group: GroupModel, request: Request, db=None) -> SCIMGroup:
|
||||
"""Convert internal Group model to SCIM Group"""
|
||||
member_ids = Groups.get_group_user_ids_by_id(group.id)
|
||||
member_ids = Groups.get_group_user_ids_by_id(group.id, db) or []
|
||||
members = []
|
||||
|
||||
for user_id in member_ids:
|
||||
user = Users.get_user_by_id(user_id)
|
||||
user = Users.get_user_by_id(user_id, db=db)
|
||||
if user:
|
||||
members.append(
|
||||
SCIMGroupMember(
|
||||
|
|
@ -483,6 +487,7 @@ async def get_users(
|
|||
count: int = Query(20, ge=1, le=100),
|
||||
filter: Optional[str] = None,
|
||||
_: bool = Depends(get_scim_auth),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""List SCIM Users"""
|
||||
skip = startIndex - 1
|
||||
|
|
@ -494,20 +499,20 @@ async def get_users(
|
|||
# In production, you'd want a more robust filter parser
|
||||
if "userName eq" in filter:
|
||||
email = filter.split('"')[1]
|
||||
user = Users.get_user_by_email(email)
|
||||
user = Users.get_user_by_email(email, db=db)
|
||||
users_list = [user] if user else []
|
||||
total = 1 if user else 0
|
||||
else:
|
||||
response = Users.get_users(skip=skip, limit=limit)
|
||||
response = Users.get_users(skip=skip, limit=limit, db=db)
|
||||
users_list = response["users"]
|
||||
total = response["total"]
|
||||
else:
|
||||
response = Users.get_users(skip=skip, limit=limit)
|
||||
response = Users.get_users(skip=skip, limit=limit, db=db)
|
||||
users_list = response["users"]
|
||||
total = response["total"]
|
||||
|
||||
# Convert to SCIM format
|
||||
scim_users = [user_to_scim(user, request) for user in users_list]
|
||||
scim_users = [user_to_scim(user, request, db=db) for user in users_list]
|
||||
|
||||
return SCIMListResponse(
|
||||
totalResults=total,
|
||||
|
|
@ -522,15 +527,16 @@ async def get_user(
|
|||
user_id: str,
|
||||
request: Request,
|
||||
_: bool = Depends(get_scim_auth),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""Get SCIM User by ID"""
|
||||
user = Users.get_user_by_id(user_id)
|
||||
user = Users.get_user_by_id(user_id, db=db)
|
||||
if not user:
|
||||
return scim_error(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail=f"User {user_id} not found"
|
||||
)
|
||||
|
||||
return user_to_scim(user, request)
|
||||
return user_to_scim(user, request, db=db)
|
||||
|
||||
|
||||
@router.post("/Users", response_model=SCIMUser, status_code=status.HTTP_201_CREATED)
|
||||
|
|
@ -538,10 +544,11 @@ async def create_user(
|
|||
request: Request,
|
||||
user_data: SCIMUserCreateRequest,
|
||||
_: bool = Depends(get_scim_auth),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""Create SCIM User"""
|
||||
# Check if user already exists
|
||||
existing_user = Users.get_user_by_email(user_data.userName)
|
||||
existing_user = Users.get_user_by_email(user_data.userName, db=db)
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
|
|
@ -572,6 +579,7 @@ async def create_user(
|
|||
email=email,
|
||||
profile_image_url=profile_image,
|
||||
role="user" if user_data.active else "pending",
|
||||
db=db,
|
||||
)
|
||||
|
||||
if not new_user:
|
||||
|
|
@ -580,7 +588,7 @@ async def create_user(
|
|||
detail="Failed to create user",
|
||||
)
|
||||
|
||||
return user_to_scim(new_user, request)
|
||||
return user_to_scim(new_user, request, db=db)
|
||||
|
||||
|
||||
@router.put("/Users/{user_id}", response_model=SCIMUser)
|
||||
|
|
@ -589,9 +597,10 @@ async def update_user(
|
|||
request: Request,
|
||||
user_data: SCIMUserUpdateRequest,
|
||||
_: bool = Depends(get_scim_auth),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""Update SCIM User (full update)"""
|
||||
user = Users.get_user_by_id(user_id)
|
||||
user = Users.get_user_by_id(user_id, db=db)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
|
|
@ -624,14 +633,14 @@ async def update_user(
|
|||
update_data["profile_image_url"] = user_data.photos[0].value
|
||||
|
||||
# Update user
|
||||
updated_user = Users.update_user_by_id(user_id, update_data)
|
||||
updated_user = Users.update_user_by_id(user_id, update_data, db=db)
|
||||
if not updated_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update user",
|
||||
)
|
||||
|
||||
return user_to_scim(updated_user, request)
|
||||
return user_to_scim(updated_user, request, db=db)
|
||||
|
||||
|
||||
@router.patch("/Users/{user_id}", response_model=SCIMUser)
|
||||
|
|
@ -640,9 +649,10 @@ async def patch_user(
|
|||
request: Request,
|
||||
patch_data: SCIMPatchRequest,
|
||||
_: bool = Depends(get_scim_auth),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""Update SCIM User (partial update)"""
|
||||
user = Users.get_user_by_id(user_id)
|
||||
user = Users.get_user_by_id(user_id, db=db)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
|
|
@ -670,7 +680,7 @@ async def patch_user(
|
|||
|
||||
# Update user
|
||||
if update_data:
|
||||
updated_user = Users.update_user_by_id(user_id, update_data)
|
||||
updated_user = Users.update_user_by_id(user_id, update_data, db=db)
|
||||
if not updated_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
|
|
@ -679,7 +689,7 @@ async def patch_user(
|
|||
else:
|
||||
updated_user = user
|
||||
|
||||
return user_to_scim(updated_user, request)
|
||||
return user_to_scim(updated_user, request, db=db)
|
||||
|
||||
|
||||
@router.delete("/Users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
|
@ -687,16 +697,17 @@ async def delete_user(
|
|||
user_id: str,
|
||||
request: Request,
|
||||
_: bool = Depends(get_scim_auth),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""Delete SCIM User"""
|
||||
user = Users.get_user_by_id(user_id)
|
||||
user = Users.get_user_by_id(user_id, db=db)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"User {user_id} not found",
|
||||
)
|
||||
|
||||
success = Users.delete_user_by_id(user_id)
|
||||
success = Users.delete_user_by_id(user_id, db=db)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
|
|
@ -714,10 +725,11 @@ async def get_groups(
|
|||
count: int = Query(20, ge=1, le=100),
|
||||
filter: Optional[str] = None,
|
||||
_: bool = Depends(get_scim_auth),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""List SCIM Groups"""
|
||||
# Get all groups
|
||||
groups_list = Groups.get_all_groups()
|
||||
groups_list = Groups.get_all_groups(db=db)
|
||||
|
||||
# Apply pagination
|
||||
total = len(groups_list)
|
||||
|
|
@ -726,7 +738,7 @@ async def get_groups(
|
|||
paginated_groups = groups_list[start:end]
|
||||
|
||||
# Convert to SCIM format
|
||||
scim_groups = [group_to_scim(group, request) for group in paginated_groups]
|
||||
scim_groups = [group_to_scim(group, request, db=db) for group in paginated_groups]
|
||||
|
||||
return SCIMListResponse(
|
||||
totalResults=total,
|
||||
|
|
@ -741,16 +753,17 @@ async def get_group(
|
|||
group_id: str,
|
||||
request: Request,
|
||||
_: bool = Depends(get_scim_auth),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""Get SCIM Group by ID"""
|
||||
group = Groups.get_group_by_id(group_id)
|
||||
group = Groups.get_group_by_id(group_id, db=db)
|
||||
if not group:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Group {group_id} not found",
|
||||
)
|
||||
|
||||
return group_to_scim(group, request)
|
||||
return group_to_scim(group, request, db=db)
|
||||
|
||||
|
||||
@router.post("/Groups", response_model=SCIMGroup, status_code=status.HTTP_201_CREATED)
|
||||
|
|
@ -758,6 +771,7 @@ async def create_group(
|
|||
request: Request,
|
||||
group_data: SCIMGroupCreateRequest,
|
||||
_: bool = Depends(get_scim_auth),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""Create SCIM Group"""
|
||||
# Extract member IDs
|
||||
|
|
@ -775,14 +789,14 @@ async def create_group(
|
|||
)
|
||||
|
||||
# Need to get the creating user's ID - we'll use the first admin
|
||||
admin_user = Users.get_super_admin_user()
|
||||
admin_user = Users.get_super_admin_user(db=db)
|
||||
if not admin_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="No admin user found",
|
||||
)
|
||||
|
||||
new_group = Groups.insert_new_group(admin_user.id, form)
|
||||
new_group = Groups.insert_new_group(admin_user.id, form, db=db)
|
||||
if not new_group:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
|
|
@ -798,12 +812,12 @@ async def create_group(
|
|||
description=new_group.description,
|
||||
)
|
||||
|
||||
Groups.update_group_by_id(new_group.id, update_form)
|
||||
Groups.set_group_user_ids_by_id(new_group.id, member_ids)
|
||||
Groups.update_group_by_id(new_group.id, update_form, db=db)
|
||||
Groups.set_group_user_ids_by_id(new_group.id, member_ids, db=db)
|
||||
|
||||
new_group = Groups.get_group_by_id(new_group.id)
|
||||
new_group = Groups.get_group_by_id(new_group.id, db=db)
|
||||
|
||||
return group_to_scim(new_group, request)
|
||||
return group_to_scim(new_group, request, db=db)
|
||||
|
||||
|
||||
@router.put("/Groups/{group_id}", response_model=SCIMGroup)
|
||||
|
|
@ -812,9 +826,10 @@ async def update_group(
|
|||
request: Request,
|
||||
group_data: SCIMGroupUpdateRequest,
|
||||
_: bool = Depends(get_scim_auth),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""Update SCIM Group (full update)"""
|
||||
group = Groups.get_group_by_id(group_id)
|
||||
group = Groups.get_group_by_id(group_id, db=db)
|
||||
if not group:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
|
|
@ -832,17 +847,17 @@ async def update_group(
|
|||
# Handle members if provided
|
||||
if group_data.members is not None:
|
||||
member_ids = [member.value for member in group_data.members]
|
||||
Groups.set_group_user_ids_by_id(group_id, member_ids)
|
||||
Groups.set_group_user_ids_by_id(group_id, member_ids, db=db)
|
||||
|
||||
# Update group
|
||||
updated_group = Groups.update_group_by_id(group_id, update_form)
|
||||
updated_group = Groups.update_group_by_id(group_id, update_form, db=db)
|
||||
if not updated_group:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update group",
|
||||
)
|
||||
|
||||
return group_to_scim(updated_group, request)
|
||||
return group_to_scim(updated_group, request, db=db)
|
||||
|
||||
|
||||
@router.patch("/Groups/{group_id}", response_model=SCIMGroup)
|
||||
|
|
@ -851,9 +866,10 @@ async def patch_group(
|
|||
request: Request,
|
||||
patch_data: SCIMPatchRequest,
|
||||
_: bool = Depends(get_scim_auth),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""Update SCIM Group (partial update)"""
|
||||
group = Groups.get_group_by_id(group_id)
|
||||
group = Groups.get_group_by_id(group_id, db=db)
|
||||
if not group:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
|
|
@ -878,7 +894,7 @@ async def patch_group(
|
|||
elif path == "members":
|
||||
# Replace all members
|
||||
Groups.set_group_user_ids_by_id(
|
||||
group_id, [member["value"] for member in value]
|
||||
group_id, [member["value"] for member in value], db=db
|
||||
)
|
||||
|
||||
elif op == "add":
|
||||
|
|
@ -887,22 +903,24 @@ async def patch_group(
|
|||
if isinstance(value, list):
|
||||
for member in value:
|
||||
if isinstance(member, dict) and "value" in member:
|
||||
Groups.add_users_to_group(group_id, [member["value"]])
|
||||
Groups.add_users_to_group(
|
||||
group_id, [member["value"]], db=db
|
||||
)
|
||||
elif op == "remove":
|
||||
if path and path.startswith("members[value eq"):
|
||||
# Remove specific member
|
||||
member_id = path.split('"')[1]
|
||||
Groups.remove_users_from_group(group_id, [member_id])
|
||||
Groups.remove_users_from_group(group_id, [member_id], db=db)
|
||||
|
||||
# Update group
|
||||
updated_group = Groups.update_group_by_id(group_id, update_form)
|
||||
updated_group = Groups.update_group_by_id(group_id, update_form, db=db)
|
||||
if not updated_group:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update group",
|
||||
)
|
||||
|
||||
return group_to_scim(updated_group, request)
|
||||
return group_to_scim(updated_group, request, db=db)
|
||||
|
||||
|
||||
@router.delete("/Groups/{group_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
|
@ -910,16 +928,17 @@ async def delete_group(
|
|||
group_id: str,
|
||||
request: Request,
|
||||
_: bool = Depends(get_scim_auth),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""Delete SCIM Group"""
|
||||
group = Groups.get_group_by_id(group_id)
|
||||
group = Groups.get_group_by_id(group_id, db=db)
|
||||
if not group:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Group {group_id} not found",
|
||||
)
|
||||
|
||||
success = Groups.delete_group_by_id(group_id)
|
||||
success = Groups.delete_group_by_id(group_id, db=db)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ from typing import Optional
|
|||
import time
|
||||
import re
|
||||
import aiohttp
|
||||
from open_webui.env import AIOHTTP_CLIENT_TIMEOUT
|
||||
from open_webui.models.groups import Groups
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import get_session
|
||||
|
||||
|
||||
from open_webui.models.oauth_sessions import OAuthSessions
|
||||
|
|
@ -15,6 +18,7 @@ from open_webui.models.tools import (
|
|||
ToolModel,
|
||||
ToolResponse,
|
||||
ToolUserResponse,
|
||||
ToolAccessResponse,
|
||||
Tools,
|
||||
)
|
||||
from open_webui.utils.plugin import (
|
||||
|
|
@ -51,11 +55,15 @@ def get_tool_module(request, tool_id, load_from_db=True):
|
|||
|
||||
|
||||
@router.get("/", response_model=list[ToolUserResponse])
|
||||
async def get_tools(request: Request, user=Depends(get_verified_user)):
|
||||
async def get_tools(
|
||||
request: Request,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
tools = []
|
||||
|
||||
# Local Tools
|
||||
for tool in Tools.get_tools():
|
||||
for tool in Tools.get_tools(db=db):
|
||||
tool_module = get_tool_module(request, tool.id)
|
||||
tools.append(
|
||||
ToolUserResponse(
|
||||
|
|
@ -140,12 +148,14 @@ async def get_tools(request: Request, user=Depends(get_verified_user)):
|
|||
# Admin can see all tools
|
||||
return tools
|
||||
else:
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user.id, db=db)
|
||||
}
|
||||
tools = [
|
||||
tool
|
||||
for tool in tools
|
||||
if tool.user_id == user.id
|
||||
or has_access(user.id, "read", tool.access_control, user_group_ids)
|
||||
or has_access(user.id, "read", tool.access_control, user_group_ids, db=db)
|
||||
]
|
||||
return tools
|
||||
|
||||
|
|
@ -155,13 +165,26 @@ async def get_tools(request: Request, user=Depends(get_verified_user)):
|
|||
############################
|
||||
|
||||
|
||||
@router.get("/list", response_model=list[ToolUserResponse])
|
||||
async def get_tool_list(user=Depends(get_verified_user)):
|
||||
@router.get("/list", response_model=list[ToolAccessResponse])
|
||||
async def get_tool_list(
|
||||
user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
|
||||
tools = Tools.get_tools()
|
||||
tools = Tools.get_tools(db=db)
|
||||
else:
|
||||
tools = Tools.get_tools_by_user_id(user.id, "write")
|
||||
return tools
|
||||
tools = Tools.get_tools_by_user_id(user.id, "read", db=db)
|
||||
|
||||
return [
|
||||
ToolAccessResponse(
|
||||
**tool.model_dump(),
|
||||
write_access=(
|
||||
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
or user.id == tool.user_id
|
||||
or has_access(user.id, "write", tool.access_control, db=db)
|
||||
),
|
||||
)
|
||||
for tool in tools
|
||||
]
|
||||
|
||||
|
||||
############################
|
||||
|
|
@ -218,7 +241,9 @@ async def load_tool_from_url(
|
|||
)
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with aiohttp.ClientSession(
|
||||
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
||||
) as session:
|
||||
async with session.get(
|
||||
url, headers={"Content-Type": "application/json"}
|
||||
) as resp:
|
||||
|
|
@ -245,9 +270,16 @@ async def load_tool_from_url(
|
|||
|
||||
|
||||
@router.get("/export", response_model=list[ToolModel])
|
||||
async def export_tools(request: Request, user=Depends(get_verified_user)):
|
||||
async def export_tools(
|
||||
request: Request,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if user.role != "admin" and not has_permission(
|
||||
user.id, "workspace.tools_export", request.app.state.config.USER_PERMISSIONS
|
||||
user.id,
|
||||
"workspace.tools_export",
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
db=db,
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -255,9 +287,9 @@ async def export_tools(request: Request, user=Depends(get_verified_user)):
|
|||
)
|
||||
|
||||
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
|
||||
return Tools.get_tools()
|
||||
return Tools.get_tools(db=db)
|
||||
else:
|
||||
return Tools.get_tools_by_user_id(user.id, "read")
|
||||
return Tools.get_tools_by_user_id(user.id, "read", db=db)
|
||||
|
||||
|
||||
############################
|
||||
|
|
@ -270,13 +302,17 @@ async def create_new_tools(
|
|||
request: Request,
|
||||
form_data: ToolForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if user.role != "admin" and not (
|
||||
has_permission(
|
||||
user.id, "workspace.tools", request.app.state.config.USER_PERMISSIONS
|
||||
user.id, "workspace.tools", request.app.state.config.USER_PERMISSIONS, db=db
|
||||
)
|
||||
or has_permission(
|
||||
user.id, "workspace.tools_import", request.app.state.config.USER_PERMISSIONS
|
||||
user.id,
|
||||
"workspace.tools_import",
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
db=db,
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
@ -292,7 +328,7 @@ async def create_new_tools(
|
|||
|
||||
form_data.id = form_data.id.lower()
|
||||
|
||||
tools = Tools.get_tool_by_id(form_data.id)
|
||||
tools = Tools.get_tool_by_id(form_data.id, db=db)
|
||||
if tools is None:
|
||||
try:
|
||||
form_data.content = replace_imports(form_data.content)
|
||||
|
|
@ -305,7 +341,7 @@ async def create_new_tools(
|
|||
TOOLS[form_data.id] = tool_module
|
||||
|
||||
specs = get_tool_specs(TOOLS[form_data.id])
|
||||
tools = Tools.insert_new_tool(user.id, form_data, specs)
|
||||
tools = Tools.insert_new_tool(user.id, form_data, specs, db=db)
|
||||
|
||||
tool_cache_dir = CACHE_DIR / "tools" / form_data.id
|
||||
tool_cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
|
@ -335,20 +371,34 @@ async def create_new_tools(
|
|||
############################
|
||||
|
||||
|
||||
@router.get("/id/{id}", response_model=Optional[ToolModel])
|
||||
async def get_tools_by_id(id: str, user=Depends(get_verified_user)):
|
||||
tools = Tools.get_tool_by_id(id)
|
||||
@router.get("/id/{id}", response_model=Optional[ToolAccessResponse])
|
||||
async def get_tools_by_id(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
tools = Tools.get_tool_by_id(id, db=db)
|
||||
|
||||
if tools:
|
||||
if (
|
||||
user.role == "admin"
|
||||
or tools.user_id == user.id
|
||||
or has_access(user.id, "read", tools.access_control)
|
||||
or has_access(user.id, "read", tools.access_control, db=db)
|
||||
):
|
||||
return tools
|
||||
return ToolAccessResponse(
|
||||
**tools.model_dump(),
|
||||
write_access=(
|
||||
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
or user.id == tools.user_id
|
||||
or has_access(user.id, "write", tools.access_control, db=db)
|
||||
),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
|
@ -364,8 +414,9 @@ async def update_tools_by_id(
|
|||
id: str,
|
||||
form_data: ToolForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
tools = Tools.get_tool_by_id(id)
|
||||
tools = Tools.get_tool_by_id(id, db=db)
|
||||
if not tools:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -375,7 +426,7 @@ async def update_tools_by_id(
|
|||
# Is the user the original creator, in a group with write access, or an admin
|
||||
if (
|
||||
tools.user_id != user.id
|
||||
and not has_access(user.id, "write", tools.access_control)
|
||||
and not has_access(user.id, "write", tools.access_control, db=db)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
@ -399,7 +450,7 @@ async def update_tools_by_id(
|
|||
}
|
||||
|
||||
log.debug(updated)
|
||||
tools = Tools.update_tool_by_id(id, updated)
|
||||
tools = Tools.update_tool_by_id(id, updated, db=db)
|
||||
|
||||
if tools:
|
||||
return tools
|
||||
|
|
@ -423,9 +474,12 @@ async def update_tools_by_id(
|
|||
|
||||
@router.delete("/id/{id}/delete", response_model=bool)
|
||||
async def delete_tools_by_id(
|
||||
request: Request, id: str, user=Depends(get_verified_user)
|
||||
request: Request,
|
||||
id: str,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
tools = Tools.get_tool_by_id(id)
|
||||
tools = Tools.get_tool_by_id(id, db=db)
|
||||
if not tools:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -434,7 +488,7 @@ async def delete_tools_by_id(
|
|||
|
||||
if (
|
||||
tools.user_id != user.id
|
||||
and not has_access(user.id, "write", tools.access_control)
|
||||
and not has_access(user.id, "write", tools.access_control, db=db)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
@ -442,7 +496,7 @@ async def delete_tools_by_id(
|
|||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||
)
|
||||
|
||||
result = Tools.delete_tool_by_id(id)
|
||||
result = Tools.delete_tool_by_id(id, db=db)
|
||||
if result:
|
||||
TOOLS = request.app.state.TOOLS
|
||||
if id in TOOLS:
|
||||
|
|
@ -457,11 +511,13 @@ async def delete_tools_by_id(
|
|||
|
||||
|
||||
@router.get("/id/{id}/valves", response_model=Optional[dict])
|
||||
async def get_tools_valves_by_id(id: str, user=Depends(get_verified_user)):
|
||||
tools = Tools.get_tool_by_id(id)
|
||||
async def get_tools_valves_by_id(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
tools = Tools.get_tool_by_id(id, db=db)
|
||||
if tools:
|
||||
try:
|
||||
valves = Tools.get_tool_valves_by_id(id)
|
||||
valves = Tools.get_tool_valves_by_id(id, db=db)
|
||||
return valves
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
|
|
@ -482,9 +538,12 @@ async def get_tools_valves_by_id(id: str, user=Depends(get_verified_user)):
|
|||
|
||||
@router.get("/id/{id}/valves/spec", response_model=Optional[dict])
|
||||
async def get_tools_valves_spec_by_id(
|
||||
request: Request, id: str, user=Depends(get_verified_user)
|
||||
request: Request,
|
||||
id: str,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
tools = Tools.get_tool_by_id(id)
|
||||
tools = Tools.get_tool_by_id(id, db=db)
|
||||
if tools:
|
||||
if id in request.app.state.TOOLS:
|
||||
tools_module = request.app.state.TOOLS[id]
|
||||
|
|
@ -510,9 +569,13 @@ async def get_tools_valves_spec_by_id(
|
|||
|
||||
@router.post("/id/{id}/valves/update", response_model=Optional[dict])
|
||||
async def update_tools_valves_by_id(
|
||||
request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
|
||||
request: Request,
|
||||
id: str,
|
||||
form_data: dict,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
tools = Tools.get_tool_by_id(id)
|
||||
tools = Tools.get_tool_by_id(id, db=db)
|
||||
if not tools:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -521,7 +584,7 @@ async def update_tools_valves_by_id(
|
|||
|
||||
if (
|
||||
tools.user_id != user.id
|
||||
and not has_access(user.id, "write", tools.access_control)
|
||||
and not has_access(user.id, "write", tools.access_control, db=db)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
|
|
@ -546,7 +609,7 @@ async def update_tools_valves_by_id(
|
|||
form_data = {k: v for k, v in form_data.items() if v is not None}
|
||||
valves = Valves(**form_data)
|
||||
valves_dict = valves.model_dump(exclude_unset=True)
|
||||
Tools.update_tool_valves_by_id(id, valves_dict)
|
||||
Tools.update_tool_valves_by_id(id, valves_dict, db=db)
|
||||
return valves_dict
|
||||
except Exception as e:
|
||||
log.exception(f"Failed to update tool valves by id {id}: {e}")
|
||||
|
|
@ -562,11 +625,13 @@ async def update_tools_valves_by_id(
|
|||
|
||||
|
||||
@router.get("/id/{id}/valves/user", response_model=Optional[dict])
|
||||
async def get_tools_user_valves_by_id(id: str, user=Depends(get_verified_user)):
|
||||
tools = Tools.get_tool_by_id(id)
|
||||
async def get_tools_user_valves_by_id(
|
||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
tools = Tools.get_tool_by_id(id, db=db)
|
||||
if tools:
|
||||
try:
|
||||
user_valves = Tools.get_user_valves_by_id_and_user_id(id, user.id)
|
||||
user_valves = Tools.get_user_valves_by_id_and_user_id(id, user.id, db=db)
|
||||
return user_valves
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
|
|
@ -582,9 +647,12 @@ async def get_tools_user_valves_by_id(id: str, user=Depends(get_verified_user)):
|
|||
|
||||
@router.get("/id/{id}/valves/user/spec", response_model=Optional[dict])
|
||||
async def get_tools_user_valves_spec_by_id(
|
||||
request: Request, id: str, user=Depends(get_verified_user)
|
||||
request: Request,
|
||||
id: str,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
tools = Tools.get_tool_by_id(id)
|
||||
tools = Tools.get_tool_by_id(id, db=db)
|
||||
if tools:
|
||||
if id in request.app.state.TOOLS:
|
||||
tools_module = request.app.state.TOOLS[id]
|
||||
|
|
@ -605,9 +673,13 @@ async def get_tools_user_valves_spec_by_id(
|
|||
|
||||
@router.post("/id/{id}/valves/user/update", response_model=Optional[dict])
|
||||
async def update_tools_user_valves_by_id(
|
||||
request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
|
||||
request: Request,
|
||||
id: str,
|
||||
form_data: dict,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
tools = Tools.get_tool_by_id(id)
|
||||
tools = Tools.get_tool_by_id(id, db=db)
|
||||
|
||||
if tools:
|
||||
if id in request.app.state.TOOLS:
|
||||
|
|
@ -624,7 +696,7 @@ async def update_tools_user_valves_by_id(
|
|||
user_valves = UserValves(**form_data)
|
||||
user_valves_dict = user_valves.model_dump(exclude_unset=True)
|
||||
Tools.update_user_valves_by_id_and_user_id(
|
||||
id, user.id, user_valves_dict
|
||||
id, user.id, user_valves_dict, db=db
|
||||
)
|
||||
return user_valves_dict
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
import base64
|
||||
import io
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ from open_webui.models.users import (
|
|||
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.env import STATIC_DIR
|
||||
from open_webui.internal.db import get_session
|
||||
|
||||
|
||||
from open_webui.utils.auth import (
|
||||
|
|
@ -60,6 +62,7 @@ async def get_users(
|
|||
direction: Optional[str] = None,
|
||||
page: Optional[int] = 1,
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
limit = PAGE_ITEM_COUNT
|
||||
|
||||
|
|
@ -74,19 +77,23 @@ async def get_users(
|
|||
if direction:
|
||||
filter["direction"] = direction
|
||||
|
||||
result = Users.get_users(filter=filter, skip=skip, limit=limit)
|
||||
filter["direction"] = direction
|
||||
|
||||
result = Users.get_users(filter=filter, skip=skip, limit=limit, db=db)
|
||||
|
||||
users = result["users"]
|
||||
total = result["total"]
|
||||
|
||||
# Fetch groups for all users in a single query to avoid N+1
|
||||
user_ids = [user.id for user in users]
|
||||
user_groups = Groups.get_groups_by_member_ids(user_ids, db=db)
|
||||
|
||||
return {
|
||||
"users": [
|
||||
UserGroupIdsModel(
|
||||
**{
|
||||
**user.model_dump(),
|
||||
"group_ids": [
|
||||
group.id for group in Groups.get_groups_by_member_id(user.id)
|
||||
],
|
||||
"group_ids": [group.id for group in user_groups.get(user.id, [])],
|
||||
}
|
||||
)
|
||||
for user in users
|
||||
|
|
@ -98,8 +105,9 @@ async def get_users(
|
|||
@router.get("/all", response_model=UserInfoListResponse)
|
||||
async def get_all_users(
|
||||
user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
return Users.get_users()
|
||||
return Users.get_users(db=db)
|
||||
|
||||
|
||||
@router.get("/search", response_model=UserInfoListResponse)
|
||||
|
|
@ -109,16 +117,13 @@ async def search_users(
|
|||
direction: Optional[str] = None,
|
||||
page: Optional[int] = 1,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
limit = PAGE_ITEM_COUNT
|
||||
|
||||
page = max(1, page)
|
||||
skip = (page - 1) * limit
|
||||
|
||||
filter = {}
|
||||
if query:
|
||||
filter["query"] = query
|
||||
|
||||
filter = {}
|
||||
if query:
|
||||
filter["query"] = query
|
||||
|
|
@ -127,7 +132,7 @@ async def search_users(
|
|||
if direction:
|
||||
filter["direction"] = direction
|
||||
|
||||
return Users.get_users(filter=filter, skip=skip, limit=limit)
|
||||
return Users.get_users(filter=filter, skip=skip, limit=limit, db=db)
|
||||
|
||||
|
||||
############################
|
||||
|
|
@ -136,8 +141,10 @@ async def search_users(
|
|||
|
||||
|
||||
@router.get("/groups")
|
||||
async def get_user_groups(user=Depends(get_verified_user)):
|
||||
return Groups.get_groups_by_member_id(user.id)
|
||||
async def get_user_groups(
|
||||
user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
return Groups.get_groups_by_member_id(user.id, db=db)
|
||||
|
||||
|
||||
############################
|
||||
|
|
@ -146,9 +153,13 @@ async def get_user_groups(user=Depends(get_verified_user)):
|
|||
|
||||
|
||||
@router.get("/permissions")
|
||||
async def get_user_permissisions(request: Request, user=Depends(get_verified_user)):
|
||||
async def get_user_permissisions(
|
||||
request: Request,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
user_permissions = get_permissions(
|
||||
user.id, request.app.state.config.USER_PERMISSIONS
|
||||
user.id, request.app.state.config.USER_PERMISSIONS, db=db
|
||||
)
|
||||
|
||||
return user_permissions
|
||||
|
|
@ -215,6 +226,11 @@ class FeaturesPermissions(BaseModel):
|
|||
web_search: bool = True
|
||||
image_generation: bool = True
|
||||
code_interpreter: bool = True
|
||||
memories: bool = True
|
||||
|
||||
|
||||
class SettingsPermissions(BaseModel):
|
||||
interface: bool = True
|
||||
|
||||
|
||||
class UserPermissions(BaseModel):
|
||||
|
|
@ -222,6 +238,7 @@ class UserPermissions(BaseModel):
|
|||
sharing: SharingPermissions
|
||||
chat: ChatPermissions
|
||||
features: FeaturesPermissions
|
||||
settings: SettingsPermissions
|
||||
|
||||
|
||||
@router.get("/default/permissions", response_model=UserPermissions)
|
||||
|
|
@ -239,6 +256,9 @@ async def get_default_user_permissions(request: Request, user=Depends(get_admin_
|
|||
"features": FeaturesPermissions(
|
||||
**request.app.state.config.USER_PERMISSIONS.get("features", {})
|
||||
),
|
||||
"settings": SettingsPermissions(
|
||||
**request.app.state.config.USER_PERMISSIONS.get("settings", {})
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -256,8 +276,10 @@ async def update_default_user_permissions(
|
|||
|
||||
|
||||
@router.get("/user/settings", response_model=Optional[UserSettings])
|
||||
async def get_user_settings_by_session_user(user=Depends(get_verified_user)):
|
||||
user = Users.get_user_by_id(user.id)
|
||||
async def get_user_settings_by_session_user(
|
||||
user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
user = Users.get_user_by_id(user.id, db=db)
|
||||
if user:
|
||||
return user.settings
|
||||
else:
|
||||
|
|
@ -274,12 +296,17 @@ async def get_user_settings_by_session_user(user=Depends(get_verified_user)):
|
|||
|
||||
@router.post("/user/settings/update", response_model=UserSettings)
|
||||
async def update_user_settings_by_session_user(
|
||||
request: Request, form_data: UserSettings, user=Depends(get_verified_user)
|
||||
request: Request,
|
||||
form_data: UserSettings,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
updated_user_settings = form_data.model_dump()
|
||||
ui_settings = updated_user_settings.get("ui")
|
||||
if (
|
||||
user.role != "admin"
|
||||
and "toolServers" in updated_user_settings.get("ui").keys()
|
||||
and ui_settings is not None
|
||||
and "toolServers" in ui_settings.keys()
|
||||
and not has_permission(
|
||||
user.id,
|
||||
"features.direct_tool_servers",
|
||||
|
|
@ -289,7 +316,7 @@ async def update_user_settings_by_session_user(
|
|||
# If the user is not an admin and does not have permission to use tool servers, remove the key
|
||||
updated_user_settings["ui"].pop("toolServers", None)
|
||||
|
||||
user = Users.update_user_settings_by_id(user.id, updated_user_settings)
|
||||
user = Users.update_user_settings_by_id(user.id, updated_user_settings, db=db)
|
||||
if user:
|
||||
return user.settings
|
||||
else:
|
||||
|
|
@ -305,8 +332,17 @@ async def update_user_settings_by_session_user(
|
|||
|
||||
|
||||
@router.get("/user/status")
|
||||
async def get_user_status_by_session_user(user=Depends(get_verified_user)):
|
||||
user = Users.get_user_by_id(user.id)
|
||||
async def get_user_status_by_session_user(
|
||||
request: Request,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
if not request.app.state.config.ENABLE_USER_STATUS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
|
||||
)
|
||||
user = Users.get_user_by_id(user.id, db=db)
|
||||
if user:
|
||||
return user
|
||||
else:
|
||||
|
|
@ -323,11 +359,19 @@ async def get_user_status_by_session_user(user=Depends(get_verified_user)):
|
|||
|
||||
@router.post("/user/status/update")
|
||||
async def update_user_status_by_session_user(
|
||||
form_data: UserStatus, user=Depends(get_verified_user)
|
||||
request: Request,
|
||||
form_data: UserStatus,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
user = Users.get_user_by_id(user.id)
|
||||
if not request.app.state.config.ENABLE_USER_STATUS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
|
||||
)
|
||||
user = Users.get_user_by_id(user.id, db=db)
|
||||
if user:
|
||||
user = Users.update_user_status_by_id(user.id, form_data)
|
||||
user = Users.update_user_status_by_id(user.id, form_data, db=db)
|
||||
return user
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
|
@ -342,8 +386,10 @@ async def update_user_status_by_session_user(
|
|||
|
||||
|
||||
@router.get("/user/info", response_model=Optional[dict])
|
||||
async def get_user_info_by_session_user(user=Depends(get_verified_user)):
|
||||
user = Users.get_user_by_id(user.id)
|
||||
async def get_user_info_by_session_user(
|
||||
user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
user = Users.get_user_by_id(user.id, db=db)
|
||||
if user:
|
||||
return user.info
|
||||
else:
|
||||
|
|
@ -360,14 +406,16 @@ async def get_user_info_by_session_user(user=Depends(get_verified_user)):
|
|||
|
||||
@router.post("/user/info/update", response_model=Optional[dict])
|
||||
async def update_user_info_by_session_user(
|
||||
form_data: dict, user=Depends(get_verified_user)
|
||||
form_data: dict, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
user = Users.get_user_by_id(user.id)
|
||||
user = Users.get_user_by_id(user.id, db=db)
|
||||
if user:
|
||||
if user.info is None:
|
||||
user.info = {}
|
||||
|
||||
user = Users.update_user_by_id(user.id, {"info": {**user.info, **form_data}})
|
||||
user = Users.update_user_by_id(
|
||||
user.id, {"info": {**user.info, **form_data}}, db=db
|
||||
)
|
||||
if user:
|
||||
return user.info
|
||||
else:
|
||||
|
|
@ -397,7 +445,9 @@ class UserActiveResponse(UserStatus):
|
|||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserActiveResponse)
|
||||
async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
|
||||
async def get_user_by_id(
|
||||
user_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
# Check if user_id is a shared chat
|
||||
# If it is, get the user_id from the chat
|
||||
if user_id.startswith("shared-"):
|
||||
|
|
@ -411,14 +461,14 @@ async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
|
|||
detail=ERROR_MESSAGES.USER_NOT_FOUND,
|
||||
)
|
||||
|
||||
user = Users.get_user_by_id(user_id)
|
||||
user = Users.get_user_by_id(user_id, db=db)
|
||||
if user:
|
||||
groups = Groups.get_groups_by_member_id(user_id)
|
||||
groups = Groups.get_groups_by_member_id(user_id, db=db)
|
||||
return UserActiveResponse(
|
||||
**{
|
||||
**user.model_dump(),
|
||||
"groups": [{"id": group.id, "name": group.name} for group in groups],
|
||||
"is_active": Users.is_user_active(user_id),
|
||||
"is_active": Users.is_user_active(user_id, db=db),
|
||||
}
|
||||
)
|
||||
else:
|
||||
|
|
@ -429,8 +479,10 @@ async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
|
|||
|
||||
|
||||
@router.get("/{user_id}/oauth/sessions")
|
||||
async def get_user_oauth_sessions_by_id(user_id: str, user=Depends(get_admin_user)):
|
||||
sessions = OAuthSessions.get_sessions_by_user_id(user_id)
|
||||
async def get_user_oauth_sessions_by_id(
|
||||
user_id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
sessions = OAuthSessions.get_sessions_by_user_id(user_id, db=db)
|
||||
if sessions and len(sessions) > 0:
|
||||
return sessions
|
||||
else:
|
||||
|
|
@ -446,8 +498,10 @@ async def get_user_oauth_sessions_by_id(user_id: str, user=Depends(get_admin_use
|
|||
|
||||
|
||||
@router.get("/{user_id}/profile/image")
|
||||
async def get_user_profile_image_by_id(user_id: str, user=Depends(get_verified_user)):
|
||||
user = Users.get_user_by_id(user_id)
|
||||
async def get_user_profile_image_by_id(
|
||||
user_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
user = Users.get_user_by_id(user_id, db=db)
|
||||
if user:
|
||||
if user.profile_image_url:
|
||||
# check if it's url or base64
|
||||
|
|
@ -461,11 +515,12 @@ async def get_user_profile_image_by_id(user_id: str, user=Depends(get_verified_u
|
|||
header, base64_data = user.profile_image_url.split(",", 1)
|
||||
image_data = base64.b64decode(base64_data)
|
||||
image_buffer = io.BytesIO(image_data)
|
||||
media_type = header.split(";")[0].lstrip("data:")
|
||||
|
||||
return StreamingResponse(
|
||||
image_buffer,
|
||||
media_type="image/png",
|
||||
headers={"Content-Disposition": "inline; filename=image.png"},
|
||||
media_type=media_type,
|
||||
headers={"Content-Disposition": "inline"},
|
||||
)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
|
@ -483,9 +538,11 @@ async def get_user_profile_image_by_id(user_id: str, user=Depends(get_verified_u
|
|||
|
||||
|
||||
@router.get("/{user_id}/active", response_model=dict)
|
||||
async def get_user_active_status_by_id(user_id: str, user=Depends(get_verified_user)):
|
||||
async def get_user_active_status_by_id(
|
||||
user_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||
):
|
||||
return {
|
||||
"active": Users.is_user_active(user_id),
|
||||
"active": Users.is_user_active(user_id, db=db),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -499,10 +556,11 @@ async def update_user_by_id(
|
|||
user_id: str,
|
||||
form_data: UserUpdateForm,
|
||||
session_user=Depends(get_admin_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
# Prevent modification of the primary admin user by other admins
|
||||
try:
|
||||
first_user = Users.get_first_user()
|
||||
first_user = Users.get_first_user(db=db)
|
||||
if first_user:
|
||||
if user_id == first_user.id:
|
||||
if session_user.id != user_id:
|
||||
|
|
@ -526,11 +584,11 @@ async def update_user_by_id(
|
|||
detail="Could not verify primary admin status.",
|
||||
)
|
||||
|
||||
user = Users.get_user_by_id(user_id)
|
||||
user = Users.get_user_by_id(user_id, db=db)
|
||||
|
||||
if user:
|
||||
if form_data.email.lower() != user.email:
|
||||
email_user = Users.get_user_by_email(form_data.email.lower())
|
||||
email_user = Users.get_user_by_email(form_data.email.lower(), db=db)
|
||||
if email_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -544,9 +602,9 @@ async def update_user_by_id(
|
|||
raise HTTPException(400, detail=str(e))
|
||||
|
||||
hashed = get_password_hash(form_data.password)
|
||||
Auths.update_user_password_by_id(user_id, hashed)
|
||||
Auths.update_user_password_by_id(user_id, hashed, db=db)
|
||||
|
||||
Auths.update_email_by_id(user_id, form_data.email.lower())
|
||||
Auths.update_email_by_id(user_id, form_data.email.lower(), db=db)
|
||||
updated_user = Users.update_user_by_id(
|
||||
user_id,
|
||||
{
|
||||
|
|
@ -555,6 +613,7 @@ async def update_user_by_id(
|
|||
"email": form_data.email.lower(),
|
||||
"profile_image_url": form_data.profile_image_url,
|
||||
},
|
||||
db=db,
|
||||
)
|
||||
|
||||
if updated_user:
|
||||
|
|
@ -577,10 +636,12 @@ async def update_user_by_id(
|
|||
|
||||
|
||||
@router.delete("/{user_id}", response_model=bool)
|
||||
async def delete_user_by_id(user_id: str, user=Depends(get_admin_user)):
|
||||
async def delete_user_by_id(
|
||||
user_id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
# Prevent deletion of the primary admin user
|
||||
try:
|
||||
first_user = Users.get_first_user()
|
||||
first_user = Users.get_first_user(db=db)
|
||||
if first_user and user_id == first_user.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
|
|
@ -594,7 +655,7 @@ async def delete_user_by_id(user_id: str, user=Depends(get_admin_user)):
|
|||
)
|
||||
|
||||
if user.id != user_id:
|
||||
result = Auths.delete_auth_by_id(user_id)
|
||||
result = Auths.delete_auth_by_id(user_id, db=db)
|
||||
|
||||
if result:
|
||||
return True
|
||||
|
|
@ -617,5 +678,7 @@ async def delete_user_by_id(user_id: str, user=Depends(get_admin_user)):
|
|||
|
||||
|
||||
@router.get("/{user_id}/groups")
|
||||
async def get_user_groups_by_id(user_id: str, user=Depends(get_admin_user)):
|
||||
return Groups.get_groups_by_member_id(user_id)
|
||||
async def get_user_groups_by_id(
|
||||
user_id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
|
||||
):
|
||||
return Groups.get_groups_by_member_id(user_id, db=db)
|
||||
|
|
|
|||
|
|
@ -246,7 +246,13 @@ def get_user_ids_from_room(room):
|
|||
active_session_ids = get_session_ids_from_room(room)
|
||||
|
||||
active_user_ids = list(
|
||||
set([SESSION_POOL.get(session_id)["id"] for session_id in active_session_ids])
|
||||
set(
|
||||
[
|
||||
SESSION_POOL.get(session_id)["id"]
|
||||
for session_id in active_session_ids
|
||||
if SESSION_POOL.get(session_id) is not None
|
||||
]
|
||||
)
|
||||
)
|
||||
return active_user_ids
|
||||
|
||||
|
|
|
|||
|
|
@ -1,236 +0,0 @@
|
|||
import uuid
|
||||
|
||||
from test.util.abstract_integration_test import AbstractPostgresTest
|
||||
from test.util.mock_user import mock_webui_user
|
||||
|
||||
|
||||
class TestChats(AbstractPostgresTest):
|
||||
BASE_PATH = "/api/v1/chats"
|
||||
|
||||
def setup_class(cls):
|
||||
super().setup_class()
|
||||
|
||||
def setup_method(self):
|
||||
super().setup_method()
|
||||
from open_webui.models.chats import ChatForm, Chats
|
||||
|
||||
self.chats = Chats
|
||||
self.chats.insert_new_chat(
|
||||
"2",
|
||||
ChatForm(
|
||||
**{
|
||||
"chat": {
|
||||
"name": "chat1",
|
||||
"description": "chat1 description",
|
||||
"tags": ["tag1", "tag2"],
|
||||
"history": {"currentId": "1", "messages": []},
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
def test_get_session_user_chat_list(self):
|
||||
with mock_webui_user(id="2"):
|
||||
response = self.fast_api_client.get(self.create_url("/"))
|
||||
assert response.status_code == 200
|
||||
first_chat = response.json()[0]
|
||||
assert first_chat["id"] is not None
|
||||
assert first_chat["title"] == "New Chat"
|
||||
assert first_chat["created_at"] is not None
|
||||
assert first_chat["updated_at"] is not None
|
||||
|
||||
def test_delete_all_user_chats(self):
|
||||
with mock_webui_user(id="2"):
|
||||
response = self.fast_api_client.delete(self.create_url("/"))
|
||||
assert response.status_code == 200
|
||||
assert len(self.chats.get_chats()) == 0
|
||||
|
||||
def test_get_user_chat_list_by_user_id(self):
|
||||
with mock_webui_user(id="3"):
|
||||
response = self.fast_api_client.get(self.create_url("/list/user/2"))
|
||||
assert response.status_code == 200
|
||||
first_chat = response.json()[0]
|
||||
assert first_chat["id"] is not None
|
||||
assert first_chat["title"] == "New Chat"
|
||||
assert first_chat["created_at"] is not None
|
||||
assert first_chat["updated_at"] is not None
|
||||
|
||||
def test_create_new_chat(self):
|
||||
with mock_webui_user(id="2"):
|
||||
response = self.fast_api_client.post(
|
||||
self.create_url("/new"),
|
||||
json={
|
||||
"chat": {
|
||||
"name": "chat2",
|
||||
"description": "chat2 description",
|
||||
"tags": ["tag1", "tag2"],
|
||||
}
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["archived"] is False
|
||||
assert data["chat"] == {
|
||||
"name": "chat2",
|
||||
"description": "chat2 description",
|
||||
"tags": ["tag1", "tag2"],
|
||||
}
|
||||
assert data["user_id"] == "2"
|
||||
assert data["id"] is not None
|
||||
assert data["share_id"] is None
|
||||
assert data["title"] == "New Chat"
|
||||
assert data["updated_at"] is not None
|
||||
assert data["created_at"] is not None
|
||||
assert len(self.chats.get_chats()) == 2
|
||||
|
||||
def test_get_user_chats(self):
|
||||
self.test_get_session_user_chat_list()
|
||||
|
||||
def test_get_user_archived_chats(self):
|
||||
self.chats.archive_all_chats_by_user_id("2")
|
||||
from open_webui.internal.db import Session
|
||||
|
||||
Session.commit()
|
||||
with mock_webui_user(id="2"):
|
||||
response = self.fast_api_client.get(self.create_url("/all/archived"))
|
||||
assert response.status_code == 200
|
||||
first_chat = response.json()[0]
|
||||
assert first_chat["id"] is not None
|
||||
assert first_chat["title"] == "New Chat"
|
||||
assert first_chat["created_at"] is not None
|
||||
assert first_chat["updated_at"] is not None
|
||||
|
||||
def test_get_all_user_chats_in_db(self):
|
||||
with mock_webui_user(id="4"):
|
||||
response = self.fast_api_client.get(self.create_url("/all/db"))
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) == 1
|
||||
|
||||
def test_get_archived_session_user_chat_list(self):
|
||||
self.test_get_user_archived_chats()
|
||||
|
||||
def test_archive_all_chats(self):
|
||||
with mock_webui_user(id="2"):
|
||||
response = self.fast_api_client.post(self.create_url("/archive/all"))
|
||||
assert response.status_code == 200
|
||||
assert len(self.chats.get_archived_chats_by_user_id("2")) == 1
|
||||
|
||||
def test_get_shared_chat_by_id(self):
|
||||
chat_id = self.chats.get_chats()[0].id
|
||||
self.chats.update_chat_share_id_by_id(chat_id, chat_id)
|
||||
with mock_webui_user(id="2"):
|
||||
response = self.fast_api_client.get(self.create_url(f"/share/{chat_id}"))
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == chat_id
|
||||
assert data["chat"] == {
|
||||
"name": "chat1",
|
||||
"description": "chat1 description",
|
||||
"tags": ["tag1", "tag2"],
|
||||
"history": {"currentId": "1", "messages": []},
|
||||
}
|
||||
assert data["id"] == chat_id
|
||||
assert data["share_id"] == chat_id
|
||||
assert data["title"] == "New Chat"
|
||||
|
||||
def test_get_chat_by_id(self):
|
||||
chat_id = self.chats.get_chats()[0].id
|
||||
with mock_webui_user(id="2"):
|
||||
response = self.fast_api_client.get(self.create_url(f"/{chat_id}"))
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == chat_id
|
||||
assert data["chat"] == {
|
||||
"name": "chat1",
|
||||
"description": "chat1 description",
|
||||
"tags": ["tag1", "tag2"],
|
||||
"history": {"currentId": "1", "messages": []},
|
||||
}
|
||||
assert data["share_id"] is None
|
||||
assert data["title"] == "New Chat"
|
||||
assert data["user_id"] == "2"
|
||||
|
||||
def test_update_chat_by_id(self):
|
||||
chat_id = self.chats.get_chats()[0].id
|
||||
with mock_webui_user(id="2"):
|
||||
response = self.fast_api_client.post(
|
||||
self.create_url(f"/{chat_id}"),
|
||||
json={
|
||||
"chat": {
|
||||
"name": "chat2",
|
||||
"description": "chat2 description",
|
||||
"tags": ["tag2", "tag4"],
|
||||
"title": "Just another title",
|
||||
}
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == chat_id
|
||||
assert data["chat"] == {
|
||||
"name": "chat2",
|
||||
"title": "Just another title",
|
||||
"description": "chat2 description",
|
||||
"tags": ["tag2", "tag4"],
|
||||
"history": {"currentId": "1", "messages": []},
|
||||
}
|
||||
assert data["share_id"] is None
|
||||
assert data["title"] == "Just another title"
|
||||
assert data["user_id"] == "2"
|
||||
|
||||
def test_delete_chat_by_id(self):
|
||||
chat_id = self.chats.get_chats()[0].id
|
||||
with mock_webui_user(id="2"):
|
||||
response = self.fast_api_client.delete(self.create_url(f"/{chat_id}"))
|
||||
assert response.status_code == 200
|
||||
assert response.json() is True
|
||||
|
||||
def test_clone_chat_by_id(self):
|
||||
chat_id = self.chats.get_chats()[0].id
|
||||
with mock_webui_user(id="2"):
|
||||
response = self.fast_api_client.get(self.create_url(f"/{chat_id}/clone"))
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] != chat_id
|
||||
assert data["chat"] == {
|
||||
"branchPointMessageId": "1",
|
||||
"description": "chat1 description",
|
||||
"history": {"currentId": "1", "messages": []},
|
||||
"name": "chat1",
|
||||
"originalChatId": chat_id,
|
||||
"tags": ["tag1", "tag2"],
|
||||
"title": "Clone of New Chat",
|
||||
}
|
||||
assert data["share_id"] is None
|
||||
assert data["title"] == "Clone of New Chat"
|
||||
assert data["user_id"] == "2"
|
||||
|
||||
def test_archive_chat_by_id(self):
|
||||
chat_id = self.chats.get_chats()[0].id
|
||||
with mock_webui_user(id="2"):
|
||||
response = self.fast_api_client.get(self.create_url(f"/{chat_id}/archive"))
|
||||
assert response.status_code == 200
|
||||
|
||||
chat = self.chats.get_chat_by_id(chat_id)
|
||||
assert chat.archived is True
|
||||
|
||||
def test_share_chat_by_id(self):
|
||||
chat_id = self.chats.get_chats()[0].id
|
||||
with mock_webui_user(id="2"):
|
||||
response = self.fast_api_client.post(self.create_url(f"/{chat_id}/share"))
|
||||
assert response.status_code == 200
|
||||
|
||||
chat = self.chats.get_chat_by_id(chat_id)
|
||||
assert chat.share_id is not None
|
||||
|
||||
def test_delete_shared_chat_by_id(self):
|
||||
chat_id = self.chats.get_chats()[0].id
|
||||
share_id = str(uuid.uuid4())
|
||||
self.chats.update_chat_share_id_by_id(chat_id, share_id)
|
||||
with mock_webui_user(id="2"):
|
||||
response = self.fast_api_client.delete(self.create_url(f"/{chat_id}/share"))
|
||||
assert response.status_code
|
||||
|
||||
chat = self.chats.get_chat_by_id(chat_id)
|
||||
assert chat.share_id is None
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
import docker
|
||||
import pytest
|
||||
from docker import DockerClient
|
||||
from pytest_docker.plugin import get_docker_ip
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import text, create_engine
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_fast_api_client():
|
||||
from main import app
|
||||
|
||||
with TestClient(app) as c:
|
||||
return c
|
||||
|
||||
|
||||
class AbstractIntegrationTest:
|
||||
BASE_PATH = None
|
||||
|
||||
def create_url(self, path="", query_params=None):
|
||||
if self.BASE_PATH is None:
|
||||
raise Exception("BASE_PATH is not set")
|
||||
parts = self.BASE_PATH.split("/")
|
||||
parts = [part.strip() for part in parts if part.strip() != ""]
|
||||
path_parts = path.split("/")
|
||||
path_parts = [part.strip() for part in path_parts if part.strip() != ""]
|
||||
query_parts = ""
|
||||
if query_params:
|
||||
query_parts = "&".join(
|
||||
[f"{key}={value}" for key, value in query_params.items()]
|
||||
)
|
||||
query_parts = f"?{query_parts}"
|
||||
return "/".join(parts + path_parts) + query_parts
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
pass
|
||||
|
||||
def setup_method(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
pass
|
||||
|
||||
def teardown_method(self):
|
||||
pass
|
||||
|
||||
|
||||
class AbstractPostgresTest(AbstractIntegrationTest):
|
||||
DOCKER_CONTAINER_NAME = "postgres-test-container-will-get-deleted"
|
||||
docker_client: DockerClient
|
||||
|
||||
@classmethod
|
||||
def _create_db_url(cls, env_vars_postgres: dict) -> str:
|
||||
host = get_docker_ip()
|
||||
user = env_vars_postgres["POSTGRES_USER"]
|
||||
pw = env_vars_postgres["POSTGRES_PASSWORD"]
|
||||
port = 8081
|
||||
db = env_vars_postgres["POSTGRES_DB"]
|
||||
return f"postgresql://{user}:{pw}@{host}:{port}/{db}"
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
super().setup_class()
|
||||
try:
|
||||
env_vars_postgres = {
|
||||
"POSTGRES_USER": "user",
|
||||
"POSTGRES_PASSWORD": "example",
|
||||
"POSTGRES_DB": "openwebui",
|
||||
}
|
||||
cls.docker_client = docker.from_env()
|
||||
cls.docker_client.containers.run(
|
||||
"postgres:16.2",
|
||||
detach=True,
|
||||
environment=env_vars_postgres,
|
||||
name=cls.DOCKER_CONTAINER_NAME,
|
||||
ports={5432: ("0.0.0.0", 8081)},
|
||||
command="postgres -c log_statement=all",
|
||||
)
|
||||
time.sleep(0.5)
|
||||
|
||||
database_url = cls._create_db_url(env_vars_postgres)
|
||||
os.environ["DATABASE_URL"] = database_url
|
||||
retries = 10
|
||||
db = None
|
||||
while retries > 0:
|
||||
try:
|
||||
from open_webui.config import OPEN_WEBUI_DIR
|
||||
|
||||
db = create_engine(database_url, pool_pre_ping=True)
|
||||
db = db.connect()
|
||||
log.info("postgres is ready!")
|
||||
break
|
||||
except Exception as e:
|
||||
log.warning(e)
|
||||
time.sleep(3)
|
||||
retries -= 1
|
||||
|
||||
if db:
|
||||
# import must be after setting env!
|
||||
cls.fast_api_client = get_fast_api_client()
|
||||
db.close()
|
||||
else:
|
||||
raise Exception("Could not connect to Postgres")
|
||||
except Exception as ex:
|
||||
log.error(ex)
|
||||
cls.teardown_class()
|
||||
pytest.fail(f"Could not setup test environment: {ex}")
|
||||
|
||||
def _check_db_connection(self):
|
||||
from open_webui.internal.db import Session
|
||||
|
||||
retries = 10
|
||||
while retries > 0:
|
||||
try:
|
||||
Session.execute(text("SELECT 1"))
|
||||
Session.commit()
|
||||
break
|
||||
except Exception as e:
|
||||
Session.rollback()
|
||||
log.warning(e)
|
||||
time.sleep(3)
|
||||
retries -= 1
|
||||
|
||||
def setup_method(self):
|
||||
super().setup_method()
|
||||
self._check_db_connection()
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls) -> None:
|
||||
super().teardown_class()
|
||||
cls.docker_client.containers.get(cls.DOCKER_CONTAINER_NAME).remove(force=True)
|
||||
|
||||
def teardown_method(self):
|
||||
from open_webui.internal.db import Session
|
||||
|
||||
# rollback everything not yet committed
|
||||
Session.commit()
|
||||
|
||||
# truncate all tables
|
||||
tables = [
|
||||
"auth",
|
||||
"chat",
|
||||
"chatidtag",
|
||||
"document",
|
||||
"memory",
|
||||
"model",
|
||||
"prompt",
|
||||
"tag",
|
||||
'"user"',
|
||||
]
|
||||
for table in tables:
|
||||
Session.execute(text(f"TRUNCATE TABLE {table}"))
|
||||
Session.commit()
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
from contextlib import contextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
|
||||
@contextmanager
|
||||
def mock_webui_user(**kwargs):
|
||||
from open_webui.routers.webui import app
|
||||
|
||||
with mock_user(app, **kwargs):
|
||||
yield
|
||||
|
||||
|
||||
@contextmanager
|
||||
def mock_user(app: FastAPI, **kwargs):
|
||||
from open_webui.utils.auth import (
|
||||
get_current_user,
|
||||
get_verified_user,
|
||||
get_admin_user,
|
||||
get_current_user_by_api_key,
|
||||
)
|
||||
from open_webui.models.users import User
|
||||
|
||||
def create_user():
|
||||
user_parameters = {
|
||||
"id": "1",
|
||||
"name": "John Doe",
|
||||
"email": "john.doe@openwebui.com",
|
||||
"role": "user",
|
||||
"profile_image_url": "/user.png",
|
||||
"last_active_at": 1627351200,
|
||||
"updated_at": 1627351200,
|
||||
"created_at": 162735120,
|
||||
**kwargs,
|
||||
}
|
||||
return User(**user_parameters)
|
||||
|
||||
app.dependency_overrides = {
|
||||
get_current_user: create_user,
|
||||
get_verified_user: create_user,
|
||||
get_admin_user: create_user,
|
||||
get_current_user_by_api_key: create_user,
|
||||
}
|
||||
yield
|
||||
app.dependency_overrides = {}
|
||||
6
backend/open_webui/tools/__init__.py
Normal file
6
backend/open_webui/tools/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"""
|
||||
Open WebUI Tools Package.
|
||||
|
||||
This package contains built-in tools that are automatically available
|
||||
when native function calling is enabled.
|
||||
"""
|
||||
1671
backend/open_webui/tools/builtin.py
Normal file
1671
backend/open_webui/tools/builtin.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -28,6 +28,7 @@ def fill_missing_permissions(
|
|||
def get_permissions(
|
||||
user_id: str,
|
||||
default_permissions: Dict[str, Any],
|
||||
db: Optional[Any] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get all permissions for a user by combining the permissions of all groups the user is a member of.
|
||||
|
|
@ -53,7 +54,7 @@ def get_permissions(
|
|||
) # Use the most permissive value (True > False)
|
||||
return permissions
|
||||
|
||||
user_groups = Groups.get_groups_by_member_id(user_id)
|
||||
user_groups = Groups.get_groups_by_member_id(user_id, db=db)
|
||||
|
||||
# Deep copy default permissions to avoid modifying the original dict
|
||||
permissions = json.loads(json.dumps(default_permissions))
|
||||
|
|
@ -72,6 +73,7 @@ def has_permission(
|
|||
user_id: str,
|
||||
permission_key: str,
|
||||
default_permissions: Dict[str, Any] = {},
|
||||
db: Optional[Any] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a user has a specific permission by checking the group permissions
|
||||
|
|
@ -92,7 +94,7 @@ def has_permission(
|
|||
permission_hierarchy = permission_key.split(".")
|
||||
|
||||
# Retrieve user group permissions
|
||||
user_groups = Groups.get_groups_by_member_id(user_id)
|
||||
user_groups = Groups.get_groups_by_member_id(user_id, db=db)
|
||||
|
||||
for group in user_groups:
|
||||
if get_permission(group.permissions or {}, permission_hierarchy):
|
||||
|
|
@ -127,6 +129,7 @@ def has_access(
|
|||
access_control: Optional[dict] = None,
|
||||
user_group_ids: Optional[Set[str]] = None,
|
||||
strict: bool = True,
|
||||
db: Optional[Any] = None,
|
||||
) -> bool:
|
||||
if access_control is None:
|
||||
if strict:
|
||||
|
|
@ -135,7 +138,7 @@ def has_access(
|
|||
return True
|
||||
|
||||
if user_group_ids is None:
|
||||
user_groups = Groups.get_groups_by_member_id(user_id)
|
||||
user_groups = Groups.get_groups_by_member_id(user_id, db=db)
|
||||
user_group_ids = {group.id for group in user_groups}
|
||||
|
||||
permitted_ids = get_permitted_group_and_user_ids(type, access_control)
|
||||
|
|
@ -152,10 +155,10 @@ def has_access(
|
|||
|
||||
# Get all users with access to a resource
|
||||
def get_users_with_access(
|
||||
type: str = "write", access_control: Optional[dict] = None
|
||||
type: str = "write", access_control: Optional[dict] = None, db: Optional[Any] = None
|
||||
) -> list[UserModel]:
|
||||
if access_control is None:
|
||||
result = Users.get_users(filter={"roles": ["!pending"]})
|
||||
result = Users.get_users(filter={"roles": ["!pending"]}, db=db)
|
||||
return result.get("users", [])
|
||||
|
||||
permitted_ids = get_permitted_group_and_user_ids(type, access_control)
|
||||
|
|
@ -167,8 +170,8 @@ def get_users_with_access(
|
|||
|
||||
user_ids_with_access = set(permitted_user_ids)
|
||||
|
||||
group_user_ids_map = Groups.get_group_user_ids_by_ids(permitted_group_ids)
|
||||
group_user_ids_map = Groups.get_group_user_ids_by_ids(permitted_group_ids, db=db)
|
||||
for user_ids in group_user_ids_map.values():
|
||||
user_ids_with_access.update(user_ids)
|
||||
|
||||
return Users.get_users_by_user_ids(list(user_ids_with_access))
|
||||
return Users.get_users_by_user_ids(list(user_ids_with_access), db=db)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ from opentelemetry import trace
|
|||
|
||||
from open_webui.utils.access_control import has_permission
|
||||
from open_webui.models.users import Users
|
||||
from open_webui.models.auths import Auths
|
||||
|
||||
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
|
||||
|
|
@ -42,6 +44,8 @@ from open_webui.env import (
|
|||
|
||||
from fastapi import BackgroundTasks, Depends, HTTPException, Request, Response, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import get_session
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -228,6 +232,10 @@ async def is_valid_token(request, decoded) -> bool:
|
|||
async def invalidate_token(request, token):
|
||||
decoded = decode_token(token)
|
||||
|
||||
# If token is invalid/expired, nothing to revoke
|
||||
if not decoded:
|
||||
return
|
||||
|
||||
# Require Redis to store revoked tokens
|
||||
if request.app.state.redis:
|
||||
jti = decoded.get("jti")
|
||||
|
|
@ -271,6 +279,7 @@ async def get_current_user(
|
|||
response: Response,
|
||||
background_tasks: BackgroundTasks,
|
||||
auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
token = None
|
||||
|
||||
|
|
@ -285,7 +294,7 @@ async def get_current_user(
|
|||
|
||||
# auth by api key
|
||||
if token.startswith("sk-"):
|
||||
user = get_current_user_by_api_key(request, token)
|
||||
user = get_current_user_by_api_key(request, token, db=db)
|
||||
|
||||
# Add user info to current span
|
||||
current_span = trace.get_current_span()
|
||||
|
|
@ -314,7 +323,7 @@ async def get_current_user(
|
|||
detail="Invalid token",
|
||||
)
|
||||
|
||||
user = Users.get_user_by_id(data["id"])
|
||||
user = Users.get_user_by_id(data["id"], db=db)
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -364,8 +373,8 @@ async def get_current_user(
|
|||
raise e
|
||||
|
||||
|
||||
def get_current_user_by_api_key(request, api_key: str):
|
||||
user = Users.get_user_by_api_key(api_key)
|
||||
def get_current_user_by_api_key(request, api_key: str, db: Session = None):
|
||||
user = Users.get_user_by_api_key(api_key, db=db)
|
||||
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
|
|
@ -393,7 +402,7 @@ def get_current_user_by_api_key(request, api_key: str):
|
|||
current_span.set_attribute("client.user.role", user.role)
|
||||
current_span.set_attribute("client.auth.type", "api_key")
|
||||
|
||||
Users.update_last_active_by_id(user.id)
|
||||
Users.update_last_active_by_id(user.id, db=db)
|
||||
return user
|
||||
|
||||
|
||||
|
|
@ -413,3 +422,37 @@ def get_admin_user(user=Depends(get_current_user)):
|
|||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
def create_admin_user(email: str, password: str, name: str = "Admin"):
|
||||
"""
|
||||
Create an admin user from environment variables.
|
||||
Used for headless/automated deployments.
|
||||
Returns the created user or None if creation failed.
|
||||
"""
|
||||
|
||||
if not email or not password:
|
||||
return None
|
||||
|
||||
if Users.has_users():
|
||||
log.debug("Users already exist, skipping admin creation")
|
||||
return None
|
||||
|
||||
log.info(f"Creating admin account from environment variables: {email}")
|
||||
try:
|
||||
hashed = get_password_hash(password)
|
||||
user = Auths.insert_new_auth(
|
||||
email=email.lower(),
|
||||
password=hashed,
|
||||
name=name,
|
||||
role="admin",
|
||||
)
|
||||
if user:
|
||||
log.info(f"Admin account created successfully: {email}")
|
||||
return user
|
||||
else:
|
||||
log.error("Failed to create admin account from environment variables")
|
||||
return None
|
||||
except Exception as e:
|
||||
log.error(f"Error creating admin account: {e}")
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ async def generate_chat_completion(
|
|||
form_data: dict,
|
||||
user: Any,
|
||||
bypass_filter: bool = False,
|
||||
bypass_system_prompt: bool = False,
|
||||
):
|
||||
log.debug(f"generate_chat_completion: {form_data}")
|
||||
if BYPASS_MODEL_ACCESS_CONTROL:
|
||||
|
|
@ -237,7 +238,11 @@ async def generate_chat_completion(
|
|||
yield chunk
|
||||
|
||||
response = await generate_chat_completion(
|
||||
request, form_data, user, bypass_filter=True
|
||||
request,
|
||||
form_data,
|
||||
user,
|
||||
bypass_filter=True,
|
||||
bypass_system_prompt=bypass_system_prompt,
|
||||
)
|
||||
return StreamingResponse(
|
||||
stream_wrapper(response.body_iterator),
|
||||
|
|
@ -248,7 +253,11 @@ async def generate_chat_completion(
|
|||
return {
|
||||
**(
|
||||
await generate_chat_completion(
|
||||
request, form_data, user, bypass_filter=True
|
||||
request,
|
||||
form_data,
|
||||
user,
|
||||
bypass_filter=True,
|
||||
bypass_system_prompt=bypass_system_prompt,
|
||||
)
|
||||
),
|
||||
"selected_model_id": selected_model_id,
|
||||
|
|
@ -267,6 +276,7 @@ async def generate_chat_completion(
|
|||
form_data=form_data,
|
||||
user=user,
|
||||
bypass_filter=bypass_filter,
|
||||
bypass_system_prompt=bypass_system_prompt,
|
||||
)
|
||||
if form_data.get("stream"):
|
||||
response.headers["content-type"] = "text/event-stream"
|
||||
|
|
@ -283,6 +293,7 @@ async def generate_chat_completion(
|
|||
form_data=form_data,
|
||||
user=user,
|
||||
bypass_filter=bypass_filter,
|
||||
bypass_system_prompt=bypass_system_prompt,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -310,7 +321,7 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
|
|||
try:
|
||||
data = await process_pipeline_outlet_filter(request, data, user, models)
|
||||
except Exception as e:
|
||||
return Exception(f"Error: {e}")
|
||||
raise Exception(f"Error: {e}")
|
||||
|
||||
metadata = {
|
||||
"chat_id": data["chat_id"],
|
||||
|
|
@ -346,7 +357,7 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
|
|||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
return Exception(f"Error: {e}")
|
||||
raise Exception(f"Error: {e}")
|
||||
|
||||
|
||||
async def chat_action(request: Request, action_id: str, form_data: dict, user: Any):
|
||||
|
|
@ -442,6 +453,6 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A
|
|||
data = action(**params)
|
||||
|
||||
except Exception as e:
|
||||
return Exception(f"Error: {e}")
|
||||
raise Exception(f"Error: {e}")
|
||||
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -28,9 +28,7 @@ def has_permission(db, DocumentModel, query, filter: dict, permission: str = "re
|
|||
for gid in group_ids:
|
||||
if dialect_name == "sqlite":
|
||||
group_read_conditions.append(
|
||||
DocumentModel.access_control["read"]["group_ids"].contains(
|
||||
[gid]
|
||||
)
|
||||
DocumentModel.access_control["read"]["group_ids"].contains(gid)
|
||||
)
|
||||
elif dialect_name == "postgresql":
|
||||
group_read_conditions.append(
|
||||
|
|
@ -63,9 +61,7 @@ def has_permission(db, DocumentModel, query, filter: dict, permission: str = "re
|
|||
for gid in group_ids:
|
||||
if dialect_name == "sqlite":
|
||||
group_write_conditions.append(
|
||||
DocumentModel.access_control["write"]["group_ids"].contains(
|
||||
[gid]
|
||||
)
|
||||
DocumentModel.access_control["write"]["group_ids"].contains(gid)
|
||||
)
|
||||
elif dialect_name == "postgresql":
|
||||
group_write_conditions.append(
|
||||
|
|
@ -111,9 +107,7 @@ def has_permission(db, DocumentModel, query, filter: dict, permission: str = "re
|
|||
for gid in group_ids:
|
||||
if dialect_name == "sqlite":
|
||||
group_conditions.append(
|
||||
DocumentModel.access_control[permission]["group_ids"].contains(
|
||||
[gid]
|
||||
)
|
||||
DocumentModel.access_control[permission]["group_ids"].contains(gid)
|
||||
)
|
||||
elif dialect_name == "postgresql":
|
||||
group_conditions.append(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ log = logging.getLogger(__name__)
|
|||
def apply_default_group_assignment(
|
||||
default_group_id: str,
|
||||
user_id: str,
|
||||
db=None,
|
||||
) -> None:
|
||||
"""
|
||||
Apply default group assignment to a user if default_group_id is provided.
|
||||
|
|
@ -17,7 +18,7 @@ def apply_default_group_assignment(
|
|||
"""
|
||||
if default_group_id:
|
||||
try:
|
||||
Groups.add_users_to_group(default_group_id, [user_id])
|
||||
Groups.add_users_to_group(default_group_id, [user_id], db=db)
|
||||
except Exception as e:
|
||||
log.error(
|
||||
f"Failed to add user {user_id} to default group {default_group_id}: {e}"
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ def get_history(prompt_id, base_url, api_key):
|
|||
return json.loads(response.read())
|
||||
|
||||
|
||||
def get_images(ws, prompt, client_id, base_url, api_key):
|
||||
prompt_id = queue_prompt(prompt, client_id, base_url, api_key)["prompt_id"]
|
||||
def get_images(ws, workflow, client_id, base_url, api_key):
|
||||
prompt_id = queue_prompt(workflow, client_id, base_url, api_key)["prompt_id"]
|
||||
output_images = []
|
||||
while True:
|
||||
out = ws.recv()
|
||||
|
|
@ -79,9 +79,12 @@ def get_images(ws, prompt, client_id, base_url, api_key):
|
|||
continue # previews are binary data
|
||||
|
||||
history = get_history(prompt_id, base_url, api_key)[prompt_id]
|
||||
for o in history["outputs"]:
|
||||
for node_id in history["outputs"]:
|
||||
node_output = history["outputs"][node_id]
|
||||
for node_id in history["outputs"]:
|
||||
node_output = history["outputs"][node_id]
|
||||
if node_id in workflow and workflow[node_id].get("class_type") in [
|
||||
"SaveImage",
|
||||
"PreviewImage",
|
||||
]:
|
||||
if "images" in node_output:
|
||||
for image in node_output["images"]:
|
||||
url = get_image_url(
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ from typing import TYPE_CHECKING
|
|||
from loguru import logger
|
||||
from opentelemetry import trace
|
||||
from open_webui.env import (
|
||||
AUDIT_UVICORN_LOGGER_NAMES,
|
||||
ENABLE_AUDIT_STDOUT,
|
||||
ENABLE_AUDIT_LOGS_FILE,
|
||||
AUDIT_LOGS_FILE_PATH,
|
||||
AUDIT_LOG_FILE_ROTATION_SIZE,
|
||||
AUDIT_LOG_LEVEL,
|
||||
AUDIT_LOGS_FILE_PATH,
|
||||
GLOBAL_LOG_LEVEL,
|
||||
AUDIT_UVICORN_LOGGER_NAMES,
|
||||
ENABLE_OTEL,
|
||||
ENABLE_OTEL_LOGS,
|
||||
)
|
||||
|
|
@ -130,9 +132,11 @@ def start_logger():
|
|||
sys.stdout,
|
||||
level=GLOBAL_LOG_LEVEL,
|
||||
format=stdout_format,
|
||||
filter=lambda record: "auditable" not in record["extra"],
|
||||
filter=lambda record: (
|
||||
"auditable" not in record["extra"] if ENABLE_AUDIT_STDOUT else True
|
||||
),
|
||||
)
|
||||
if AUDIT_LOG_LEVEL != "NONE":
|
||||
if AUDIT_LOG_LEVEL != "NONE" and ENABLE_AUDIT_LOGS_FILE:
|
||||
try:
|
||||
logger.add(
|
||||
AUDIT_LOGS_FILE_PATH,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ from open_webui.routers.retrieval import (
|
|||
process_web_search,
|
||||
SearchForm,
|
||||
)
|
||||
from open_webui.utils.tools import get_builtin_tools
|
||||
from open_webui.routers.images import (
|
||||
image_generations,
|
||||
CreateImageForm,
|
||||
|
|
@ -92,7 +93,11 @@ from open_webui.utils.misc import (
|
|||
convert_logit_bias_input_to_json,
|
||||
get_content_from_message,
|
||||
)
|
||||
from open_webui.utils.tools import get_tools, get_updated_tool_function
|
||||
from open_webui.utils.tools import (
|
||||
get_tools,
|
||||
get_updated_tool_function,
|
||||
has_tool_server_access,
|
||||
)
|
||||
from open_webui.utils.plugin import load_function_module_by_id
|
||||
from open_webui.utils.filter import (
|
||||
get_sorted_filter_ids,
|
||||
|
|
@ -118,6 +123,7 @@ from open_webui.env import (
|
|||
BYPASS_MODEL_ACCESS_CONTROL,
|
||||
ENABLE_REALTIME_CHAT_SAVE,
|
||||
ENABLE_QUERIES_CACHE,
|
||||
RAG_SYSTEM_CONTEXT,
|
||||
)
|
||||
from open_webui.constants import TASKS
|
||||
|
||||
|
|
@ -140,6 +146,196 @@ DEFAULT_SOLUTION_TAGS = [("<|begin_of_solution|>", "<|end_of_solution|>")]
|
|||
DEFAULT_CODE_INTERPRETER_TAGS = [("<code_interpreter>", "</code_interpreter>")]
|
||||
|
||||
|
||||
def get_citation_source_from_tool_result(
|
||||
tool_name: str, tool_params: dict, tool_result: str, tool_id: str = ""
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Parse a tool's result and convert it to source dicts for citation display.
|
||||
|
||||
Follows the source format conventions from get_sources_from_items:
|
||||
- source: file/item info object with id, name, type
|
||||
- document: list of document contents
|
||||
- metadata: list of metadata objects with source, file_id, name fields
|
||||
|
||||
Returns a list of sources (usually one, but query_knowledge_files may return multiple).
|
||||
"""
|
||||
try:
|
||||
if tool_name == "search_web":
|
||||
# Parse JSON array: [{"title": "...", "link": "...", "snippet": "..."}]
|
||||
results = json.loads(tool_result)
|
||||
documents = []
|
||||
metadata = []
|
||||
|
||||
for result in results:
|
||||
title = result.get("title", "")
|
||||
link = result.get("link", "")
|
||||
snippet = result.get("snippet", "")
|
||||
|
||||
documents.append(f"{title}\n{snippet}")
|
||||
metadata.append(
|
||||
{
|
||||
"source": link,
|
||||
"name": title,
|
||||
"url": link,
|
||||
}
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
"source": {"name": "search_web", "id": "search_web"},
|
||||
"document": documents,
|
||||
"metadata": metadata,
|
||||
}
|
||||
]
|
||||
|
||||
elif tool_name == "view_knowledge_file":
|
||||
file_data = json.loads(tool_result)
|
||||
filename = file_data.get("filename", "Unknown File")
|
||||
file_id = file_data.get("id", "")
|
||||
knowledge_name = file_data.get("knowledge_name", "")
|
||||
|
||||
return [
|
||||
{
|
||||
"source": {
|
||||
"id": file_id,
|
||||
"name": filename,
|
||||
"type": "file",
|
||||
},
|
||||
"document": [file_data.get("content", "")],
|
||||
"metadata": [
|
||||
{
|
||||
"file_id": file_id,
|
||||
"name": filename,
|
||||
"source": filename,
|
||||
**(
|
||||
{"knowledge_name": knowledge_name}
|
||||
if knowledge_name
|
||||
else {}
|
||||
),
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
elif tool_name == "query_knowledge_files":
|
||||
chunks = json.loads(tool_result)
|
||||
|
||||
# Group chunks by source for better citation display
|
||||
# Each unique source becomes a separate source entry
|
||||
sources_by_file = {}
|
||||
|
||||
for chunk in chunks:
|
||||
source_name = chunk.get("source", "Unknown")
|
||||
file_id = chunk.get("file_id", "")
|
||||
note_id = chunk.get("note_id", "")
|
||||
chunk_type = chunk.get("type", "file")
|
||||
content = chunk.get("content", "")
|
||||
|
||||
# Use file_id or note_id as the key
|
||||
key = file_id or note_id or source_name
|
||||
|
||||
if key not in sources_by_file:
|
||||
sources_by_file[key] = {
|
||||
"source": {
|
||||
"id": file_id or note_id,
|
||||
"name": source_name,
|
||||
"type": chunk_type,
|
||||
},
|
||||
"document": [],
|
||||
"metadata": [],
|
||||
}
|
||||
|
||||
sources_by_file[key]["document"].append(content)
|
||||
sources_by_file[key]["metadata"].append(
|
||||
{
|
||||
"file_id": file_id,
|
||||
"name": source_name,
|
||||
"source": source_name,
|
||||
**({"note_id": note_id} if note_id else {}),
|
||||
}
|
||||
)
|
||||
|
||||
# Return all grouped sources as a list
|
||||
if sources_by_file:
|
||||
return list(sources_by_file.values())
|
||||
|
||||
# Empty result fallback
|
||||
return []
|
||||
|
||||
else:
|
||||
# Fallback for other tools
|
||||
return [
|
||||
{
|
||||
"source": {
|
||||
"name": tool_name,
|
||||
"type": "tool",
|
||||
"id": tool_id or tool_name,
|
||||
},
|
||||
"document": [str(tool_result)],
|
||||
"metadata": [{"source": tool_name, "name": tool_name}],
|
||||
}
|
||||
]
|
||||
except Exception as e:
|
||||
log.exception(f"Error parsing tool result for {tool_name}: {e}")
|
||||
return [
|
||||
{
|
||||
"source": {"name": tool_name, "type": "tool"},
|
||||
"document": [str(tool_result)],
|
||||
"metadata": [{"source": tool_name}],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def apply_source_context_to_messages(
|
||||
request: Request,
|
||||
messages: list,
|
||||
sources: list,
|
||||
user_message: str,
|
||||
) -> list:
|
||||
"""
|
||||
Build source context from citation sources and apply to messages.
|
||||
Uses RAG template to format context for model consumption.
|
||||
"""
|
||||
if not sources or not user_message:
|
||||
return messages
|
||||
|
||||
context_string = ""
|
||||
citation_idx = {}
|
||||
|
||||
for source in sources:
|
||||
for doc, meta in zip(source.get("document", []), source.get("metadata", [])):
|
||||
src_id = meta.get("source") or source.get("source", {}).get("id") or "N/A"
|
||||
if src_id not in citation_idx:
|
||||
citation_idx[src_id] = len(citation_idx) + 1
|
||||
src_name = source.get("source", {}).get("name")
|
||||
context_string += (
|
||||
f'<source id="{citation_idx[src_id]}"'
|
||||
+ (f' name="{src_name}"' if src_name else "")
|
||||
+ f">{doc}</source>\n"
|
||||
)
|
||||
|
||||
context_string = context_string.strip()
|
||||
if not context_string:
|
||||
return messages
|
||||
|
||||
if RAG_SYSTEM_CONTEXT:
|
||||
return add_or_update_system_message(
|
||||
rag_template(
|
||||
request.app.state.config.RAG_TEMPLATE, context_string, user_message
|
||||
),
|
||||
messages,
|
||||
append=True,
|
||||
)
|
||||
else:
|
||||
return add_or_update_user_message(
|
||||
rag_template(
|
||||
request.app.state.config.RAG_TEMPLATE, context_string, user_message
|
||||
),
|
||||
messages,
|
||||
append=False,
|
||||
)
|
||||
|
||||
|
||||
def process_tool_result(
|
||||
request,
|
||||
tool_function_name,
|
||||
|
|
@ -724,6 +920,8 @@ def get_images_from_messages(message_list):
|
|||
for file in message.get("files", []):
|
||||
if file.get("type") == "image":
|
||||
message_images.append(file.get("url"))
|
||||
elif file.get("content_type", "").startswith("image/"):
|
||||
message_images.append(file.get("url"))
|
||||
|
||||
if message_images:
|
||||
images.append(message_images)
|
||||
|
|
@ -752,6 +950,51 @@ def get_image_urls(delta_images, request, metadata, user) -> list[str]:
|
|||
return image_urls
|
||||
|
||||
|
||||
def add_file_context(messages: list, chat_id: str, user) -> list:
|
||||
"""
|
||||
Add file URLs to messages for native function calling.
|
||||
"""
|
||||
if not chat_id or chat_id.startswith("local:"):
|
||||
return messages
|
||||
|
||||
chat = Chats.get_chat_by_id_and_user_id(chat_id, user.id)
|
||||
if not chat:
|
||||
return messages
|
||||
|
||||
history = chat.chat.get("history", {})
|
||||
stored_messages = get_message_list(
|
||||
history.get("messages", {}), history.get("currentId")
|
||||
)
|
||||
|
||||
def format_file_tag(file):
|
||||
attrs = f'type="{file.get("type", "file")}" url="{file["url"]}"'
|
||||
if file.get("content_type"):
|
||||
attrs += f' content_type="{file["content_type"]}"'
|
||||
if file.get("name"):
|
||||
attrs += f' name="{file["name"]}"'
|
||||
return f"<file {attrs}/>"
|
||||
|
||||
for message, stored_message in zip(messages, stored_messages):
|
||||
files_with_urls = [
|
||||
file for file in stored_message.get("files", []) if file.get("url")
|
||||
]
|
||||
if not files_with_urls:
|
||||
continue
|
||||
|
||||
file_tags = [format_file_tag(file) for file in files_with_urls]
|
||||
file_context = (
|
||||
"<attached_files>\n" + "\n".join(file_tags) + "\n</attached_files>\n\n"
|
||||
)
|
||||
|
||||
content = message.get("content", "")
|
||||
if isinstance(content, list):
|
||||
message["content"] = [{"type": "text", "text": file_context}] + content
|
||||
else:
|
||||
message["content"] = file_context + content
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
async def chat_image_generation_handler(
|
||||
request: Request, form_data: dict, extra_params: dict, user
|
||||
):
|
||||
|
|
@ -1196,6 +1439,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
"__oauth_token__": oauth_token,
|
||||
"__request__": request,
|
||||
"__model__": model,
|
||||
"__chat_id__": metadata.get("chat_id"),
|
||||
"__message_id__": metadata.get("message_id"),
|
||||
}
|
||||
# Initialize events to store additional event to be sent to the client
|
||||
# Initialize contexts and citation
|
||||
|
|
@ -1239,7 +1484,10 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
user_message = get_last_user_message(form_data["messages"])
|
||||
model_knowledge = model.get("info", {}).get("meta", {}).get("knowledge", False)
|
||||
|
||||
if model_knowledge:
|
||||
if (
|
||||
model_knowledge
|
||||
and metadata.get("params", {}).get("function_calling") != "native"
|
||||
):
|
||||
await event_emitter(
|
||||
{
|
||||
"type": "status",
|
||||
|
|
@ -1305,7 +1553,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
except Exception as e:
|
||||
raise Exception(f"{e}")
|
||||
|
||||
features = form_data.pop("features", None)
|
||||
features = form_data.pop("features", None) or {}
|
||||
extra_params["__features__"] = features
|
||||
if features:
|
||||
if "voice" in features and features["voice"]:
|
||||
if request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE != None:
|
||||
|
|
@ -1320,19 +1569,25 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
)
|
||||
|
||||
if "memory" in features and features["memory"]:
|
||||
form_data = await chat_memory_handler(
|
||||
request, form_data, extra_params, user
|
||||
)
|
||||
# Skip forced memory injection when native FC is enabled - model can use memory tools
|
||||
if metadata.get("params", {}).get("function_calling") != "native":
|
||||
form_data = await chat_memory_handler(
|
||||
request, form_data, extra_params, user
|
||||
)
|
||||
|
||||
if "web_search" in features and features["web_search"]:
|
||||
form_data = await chat_web_search_handler(
|
||||
request, form_data, extra_params, user
|
||||
)
|
||||
# Skip forced RAG web search when native FC is enabled - model can use web_search tool
|
||||
if metadata.get("params", {}).get("function_calling") != "native":
|
||||
form_data = await chat_web_search_handler(
|
||||
request, form_data, extra_params, user
|
||||
)
|
||||
|
||||
if "image_generation" in features and features["image_generation"]:
|
||||
form_data = await chat_image_generation_handler(
|
||||
request, form_data, extra_params, user
|
||||
)
|
||||
# Skip forced image generation when native FC is enabled - model can use generate_image tool
|
||||
if metadata.get("params", {}).get("function_calling") != "native":
|
||||
form_data = await chat_image_generation_handler(
|
||||
request, form_data, extra_params, user
|
||||
)
|
||||
|
||||
if "code_interpreter" in features and features["code_interpreter"]:
|
||||
form_data["messages"] = add_or_update_user_message(
|
||||
|
|
@ -1412,6 +1667,13 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
log.error(f"MCP server with id {server_id} not found")
|
||||
continue
|
||||
|
||||
# Check access control for MCP server
|
||||
if not has_tool_server_access(user, mcp_server_connection):
|
||||
log.warning(
|
||||
f"Access denied to MCP server {server_id} for user {user.id}"
|
||||
)
|
||||
continue
|
||||
|
||||
auth_type = mcp_server_connection.get("auth_type", "")
|
||||
headers = {}
|
||||
if auth_type == "bearer":
|
||||
|
|
@ -1543,6 +1805,36 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
if mcp_clients:
|
||||
metadata["mcp_clients"] = mcp_clients
|
||||
|
||||
# Inject builtin tools for native function calling based on enabled features and model capability
|
||||
# Check if builtin_tools capability is enabled for this model (defaults to True if not specified)
|
||||
builtin_tools_enabled = (
|
||||
model.get("info", {})
|
||||
.get("meta", {})
|
||||
.get("capabilities", {})
|
||||
.get("builtin_tools", True)
|
||||
)
|
||||
if (
|
||||
metadata.get("params", {}).get("function_calling") == "native"
|
||||
and builtin_tools_enabled
|
||||
):
|
||||
# Add file context to user messages
|
||||
chat_id = metadata.get("chat_id")
|
||||
form_data["messages"] = add_file_context(
|
||||
form_data.get("messages", []), chat_id, user
|
||||
)
|
||||
builtin_tools = get_builtin_tools(
|
||||
request,
|
||||
{
|
||||
**extra_params,
|
||||
"__event_emitter__": event_emitter,
|
||||
},
|
||||
features,
|
||||
model,
|
||||
)
|
||||
for name, tool_dict in builtin_tools.items():
|
||||
if name not in tools_dict:
|
||||
tools_dict[name] = tool_dict
|
||||
|
||||
if tools_dict:
|
||||
if metadata.get("params", {}).get("function_calling") == "native":
|
||||
# If the function calling is native, then call the tools function calling handler
|
||||
|
|
@ -1551,6 +1843,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
{"type": "function", "function": tool.get("spec", {})}
|
||||
for tool in tools_dict.values()
|
||||
]
|
||||
|
||||
else:
|
||||
# If the function calling is not native, then call the tools function calling handler
|
||||
try:
|
||||
|
|
@ -1561,54 +1854,28 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
try:
|
||||
form_data, flags = await chat_completion_files_handler(
|
||||
request, form_data, extra_params, user
|
||||
)
|
||||
sources.extend(flags.get("sources", []))
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
# Check if file context extraction is enabled for this model (default True)
|
||||
file_context_enabled = (
|
||||
model.get("info", {})
|
||||
.get("meta", {})
|
||||
.get("capabilities", {})
|
||||
.get("file_context", True)
|
||||
)
|
||||
|
||||
if file_context_enabled:
|
||||
try:
|
||||
form_data, flags = await chat_completion_files_handler(
|
||||
request, form_data, extra_params, user
|
||||
)
|
||||
sources.extend(flags.get("sources", []))
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
# If context is not empty, insert it into the messages
|
||||
if len(sources) > 0:
|
||||
context_string = ""
|
||||
citation_idx_map = {}
|
||||
|
||||
for source in sources:
|
||||
if "document" in source:
|
||||
for document_text, document_metadata in zip(
|
||||
source["document"], source["metadata"]
|
||||
):
|
||||
source_name = source.get("source", {}).get("name", None)
|
||||
source_id = (
|
||||
document_metadata.get("source", None)
|
||||
or source.get("source", {}).get("id", None)
|
||||
or "N/A"
|
||||
)
|
||||
|
||||
if source_id not in citation_idx_map:
|
||||
citation_idx_map[source_id] = len(citation_idx_map) + 1
|
||||
|
||||
context_string += (
|
||||
f'<source id="{citation_idx_map[source_id]}"'
|
||||
+ (f' name="{source_name}"' if source_name else "")
|
||||
+ f">{document_text}</source>\n"
|
||||
)
|
||||
|
||||
context_string = context_string.strip()
|
||||
if prompt is None:
|
||||
raise Exception("No user message found")
|
||||
|
||||
if context_string != "":
|
||||
form_data["messages"] = add_or_update_user_message(
|
||||
rag_template(
|
||||
request.app.state.config.RAG_TEMPLATE,
|
||||
context_string,
|
||||
prompt,
|
||||
),
|
||||
form_data["messages"],
|
||||
append=False,
|
||||
)
|
||||
if sources and prompt:
|
||||
form_data["messages"] = apply_source_context_to_messages(
|
||||
request, form_data["messages"], sources, prompt
|
||||
)
|
||||
|
||||
# If there are citations, add them to the data_items
|
||||
sources = [
|
||||
|
|
@ -2605,8 +2872,42 @@ async def process_chat_response(
|
|||
continue
|
||||
|
||||
delta = choices[0].get("delta", {})
|
||||
delta_tool_calls = delta.get("tool_calls", None)
|
||||
|
||||
# Handle delta annotations
|
||||
annotations = delta.get("annotations")
|
||||
if annotations:
|
||||
for annotation in annotations:
|
||||
if (
|
||||
annotation.get("type") == "url_citation"
|
||||
and "url_citation" in annotation
|
||||
):
|
||||
url_citation = annotation[
|
||||
"url_citation"
|
||||
]
|
||||
|
||||
url = url_citation.get("url", "")
|
||||
title = url_citation.get("title", url)
|
||||
|
||||
await event_emitter(
|
||||
{
|
||||
"type": "source",
|
||||
"data": {
|
||||
"source": {
|
||||
"name": title,
|
||||
"url": url,
|
||||
},
|
||||
"document": [title],
|
||||
"metadata": [
|
||||
{
|
||||
"source": url,
|
||||
"name": title,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
delta_tool_calls = delta.get("tool_calls", None)
|
||||
if delta_tool_calls:
|
||||
for delta_tool_call in delta_tool_calls:
|
||||
tool_call_index = delta_tool_call.get(
|
||||
|
|
@ -2665,6 +2966,29 @@ async def process_chat_response(
|
|||
"arguments"
|
||||
] += delta_arguments
|
||||
|
||||
# Emit pending tool calls in real-time
|
||||
if response_tool_calls:
|
||||
# Flush any pending text first
|
||||
await flush_pending_delta_data()
|
||||
|
||||
pending_content_blocks = content_blocks + [
|
||||
{
|
||||
"type": "tool_calls",
|
||||
"content": response_tool_calls,
|
||||
"pending": True,
|
||||
}
|
||||
]
|
||||
await event_emitter(
|
||||
{
|
||||
"type": "chat:completion",
|
||||
"data": {
|
||||
"content": serialize_content_blocks(
|
||||
pending_content_blocks
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
image_urls = get_image_urls(
|
||||
delta.get("images", []), request, metadata, user
|
||||
)
|
||||
|
|
@ -2878,6 +3202,7 @@ async def process_chat_response(
|
|||
await stream_body_handler(response, form_data)
|
||||
|
||||
tool_call_retries = 0
|
||||
tool_call_sources = [] # Track citation sources from tool results
|
||||
|
||||
while (
|
||||
len(tool_calls) > 0
|
||||
|
|
@ -3012,6 +3337,27 @@ async def process_chat_response(
|
|||
)
|
||||
)
|
||||
|
||||
# Extract citation sources from tool results
|
||||
if (
|
||||
tool_function_name
|
||||
in [
|
||||
"search_web",
|
||||
"view_knowledge_file",
|
||||
"query_knowledge_files",
|
||||
]
|
||||
and tool_result
|
||||
):
|
||||
try:
|
||||
citation_sources = get_citation_source_from_tool_result(
|
||||
tool_name=tool_function_name,
|
||||
tool_params=tool_function_params,
|
||||
tool_result=tool_result,
|
||||
tool_id=tool.get("tool_id", "") if tool else "",
|
||||
)
|
||||
tool_call_sources.extend(citation_sources)
|
||||
except Exception as e:
|
||||
log.exception(f"Error extracting citation source: {e}")
|
||||
|
||||
results.append(
|
||||
{
|
||||
"tool_call_id": tool_call_id,
|
||||
|
|
@ -3037,6 +3383,22 @@ async def process_chat_response(
|
|||
}
|
||||
)
|
||||
|
||||
# Emit citation sources for UI display
|
||||
for source in tool_call_sources:
|
||||
await event_emitter({"type": "source", "data": source})
|
||||
|
||||
# Apply source context to messages for model
|
||||
if tool_call_sources:
|
||||
user_msg = get_last_user_message(form_data["messages"])
|
||||
if user_msg:
|
||||
form_data["messages"] = apply_source_context_to_messages(
|
||||
request,
|
||||
form_data["messages"],
|
||||
tool_call_sources,
|
||||
user_msg,
|
||||
)
|
||||
tool_call_sources.clear()
|
||||
|
||||
await event_emitter(
|
||||
{
|
||||
"type": "chat:completion",
|
||||
|
|
@ -3063,6 +3425,7 @@ async def process_chat_response(
|
|||
request,
|
||||
new_form_data,
|
||||
user,
|
||||
bypass_system_prompt=True,
|
||||
)
|
||||
|
||||
if isinstance(res, StreamingResponse):
|
||||
|
|
@ -3242,6 +3605,7 @@ async def process_chat_response(
|
|||
request,
|
||||
new_form_data,
|
||||
user,
|
||||
bypass_system_prompt=True,
|
||||
)
|
||||
|
||||
if isinstance(res, StreamingResponse):
|
||||
|
|
|
|||
|
|
@ -337,7 +337,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
|
|||
return models
|
||||
|
||||
|
||||
def check_model_access(user, model):
|
||||
def check_model_access(user, model, db=None):
|
||||
if model.get("arena"):
|
||||
if not has_access(
|
||||
user.id,
|
||||
|
|
@ -345,22 +345,23 @@ def check_model_access(user, model):
|
|||
access_control=model.get("info", {})
|
||||
.get("meta", {})
|
||||
.get("access_control", {}),
|
||||
db=db,
|
||||
):
|
||||
raise Exception("Model not found")
|
||||
else:
|
||||
model_info = Models.get_model_by_id(model.get("id"))
|
||||
model_info = Models.get_model_by_id(model.get("id"), db=db)
|
||||
if not model_info:
|
||||
raise Exception("Model not found")
|
||||
elif not (
|
||||
user.id == model_info.user_id
|
||||
or has_access(
|
||||
user.id, type="read", access_control=model_info.access_control
|
||||
user.id, type="read", access_control=model_info.access_control, db=db
|
||||
)
|
||||
):
|
||||
raise Exception("Model not found")
|
||||
|
||||
|
||||
def get_filtered_models(models, user):
|
||||
def get_filtered_models(models, user, db=None):
|
||||
# Filter out models that the user does not have access to
|
||||
if (
|
||||
user.role == "user"
|
||||
|
|
@ -373,7 +374,9 @@ def get_filtered_models(models, user):
|
|||
}
|
||||
|
||||
filtered_models = []
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user.id, db=db)
|
||||
}
|
||||
for model in models:
|
||||
if model.get("arena"):
|
||||
if has_access(
|
||||
|
|
|
|||
|
|
@ -246,10 +246,66 @@ def get_parsed_and_base_url(server_url) -> tuple[urllib.parse.ParseResult, str]:
|
|||
return parsed, base_url
|
||||
|
||||
|
||||
def get_discovery_urls(server_url) -> list[str]:
|
||||
parsed, base_url = get_parsed_and_base_url(server_url)
|
||||
async def get_authorization_server_discovery_urls(server_url: str) -> list[str]:
|
||||
"""
|
||||
https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization
|
||||
"""
|
||||
|
||||
urls = []
|
||||
authorization_servers = []
|
||||
try:
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with session.post(
|
||||
server_url,
|
||||
json={"jsonrpc": "2.0", "method": "initialize", "params": {}, "id": 1},
|
||||
headers={"Content-Type": "application/json"},
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
) as response:
|
||||
if response.status == 401:
|
||||
match = re.search(
|
||||
r'resource_metadata="([^"]+)"',
|
||||
response.headers.get("WWW-Authenticate", ""),
|
||||
)
|
||||
if match:
|
||||
resource_metadata_url = match.group(1)
|
||||
log.debug(
|
||||
f"Found resource_metadata URL: {resource_metadata_url}"
|
||||
)
|
||||
|
||||
# Step 2: Fetch Protected Resource metadata
|
||||
async with session.get(
|
||||
resource_metadata_url, ssl=AIOHTTP_CLIENT_SESSION_SSL
|
||||
) as resource_response:
|
||||
if resource_response.status == 200:
|
||||
resource_metadata = await resource_response.json()
|
||||
|
||||
# Step 3: Extract authorization_servers
|
||||
servers = resource_metadata.get(
|
||||
"authorization_servers", []
|
||||
)
|
||||
if servers:
|
||||
authorization_servers = servers
|
||||
log.debug(
|
||||
f"Discovered authorization servers: {servers}"
|
||||
)
|
||||
except Exception as e:
|
||||
log.debug(f"MCP Protected Resource discovery failed: {e}")
|
||||
|
||||
discovery_urls = []
|
||||
for auth_server in authorization_servers:
|
||||
auth_server = auth_server.rstrip("/")
|
||||
discovery_urls.extend(
|
||||
[
|
||||
f"{auth_server}/.well-known/oauth-authorization-server",
|
||||
f"{auth_server}/.well-known/openid-configuration",
|
||||
]
|
||||
)
|
||||
|
||||
return discovery_urls
|
||||
|
||||
|
||||
async def get_discovery_urls(server_url) -> list[str]:
|
||||
urls = await get_authorization_server_discovery_urls(server_url)
|
||||
parsed, base_url = get_parsed_and_base_url(server_url)
|
||||
|
||||
if parsed.path and parsed.path != "/":
|
||||
# Generate discovery URLs based on https://modelcontextprotocol.io/specification/draft/basic/authorization#authorization-server-metadata-discovery
|
||||
|
|
@ -303,7 +359,7 @@ async def get_oauth_client_info_with_dynamic_client_registration(
|
|||
)
|
||||
|
||||
# Attempt to fetch OAuth server metadata to get registration endpoint & scopes
|
||||
discovery_urls = get_discovery_urls(oauth_server_url)
|
||||
discovery_urls = await get_discovery_urls(oauth_server_url)
|
||||
for url in discovery_urls:
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with session.get(
|
||||
|
|
@ -579,24 +635,27 @@ class OAuthClientManager:
|
|||
def get_client(self, client_id):
|
||||
if client_id not in self.clients:
|
||||
self.ensure_client_from_config(client_id)
|
||||
|
||||
client = self.clients.get(client_id)
|
||||
return client["client"] if client else None
|
||||
|
||||
def get_client_info(self, client_id):
|
||||
if client_id not in self.clients:
|
||||
self.ensure_client_from_config(client_id)
|
||||
|
||||
client = self.clients.get(client_id)
|
||||
return client["client_info"] if client else None
|
||||
|
||||
def get_server_metadata_url(self, client_id):
|
||||
if client_id in self.clients:
|
||||
client = self.clients[client_id]
|
||||
return (
|
||||
client._server_metadata_url
|
||||
if hasattr(client, "_server_metadata_url")
|
||||
else None
|
||||
)
|
||||
return None
|
||||
client = self.get_client(client_id)
|
||||
if not client:
|
||||
return None
|
||||
|
||||
return (
|
||||
client._server_metadata_url
|
||||
if hasattr(client, "_server_metadata_url")
|
||||
else None
|
||||
)
|
||||
|
||||
async def get_oauth_token(
|
||||
self, user_id: str, client_id: str, force_refresh: bool = False
|
||||
|
|
@ -1132,7 +1191,7 @@ class OAuthManager:
|
|||
|
||||
return role
|
||||
|
||||
def update_user_groups(self, user, user_data, default_permissions):
|
||||
def update_user_groups(self, user, user_data, default_permissions, db=None):
|
||||
log.debug("Running OAUTH Group management")
|
||||
oauth_claim = auth_manager_config.OAUTH_GROUPS_CLAIM
|
||||
|
||||
|
|
@ -1161,8 +1220,10 @@ class OAuthManager:
|
|||
else:
|
||||
user_oauth_groups = []
|
||||
|
||||
user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id)
|
||||
all_available_groups: list[GroupModel] = Groups.get_all_groups()
|
||||
user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(
|
||||
user.id, db=db
|
||||
)
|
||||
all_available_groups: list[GroupModel] = Groups.get_all_groups(db=db)
|
||||
|
||||
# Create groups if they don't exist and creation is enabled
|
||||
if auth_manager_config.ENABLE_OAUTH_GROUP_CREATION:
|
||||
|
|
@ -1188,7 +1249,7 @@ class OAuthManager:
|
|||
)
|
||||
# Use determined creator ID (admin or fallback to current user)
|
||||
created_group = Groups.insert_new_group(
|
||||
creator_id, new_group_form
|
||||
creator_id, new_group_form, db=db
|
||||
)
|
||||
if created_group:
|
||||
log.info(
|
||||
|
|
@ -1206,7 +1267,7 @@ class OAuthManager:
|
|||
|
||||
# Refresh the list of all available groups if any were created
|
||||
if groups_created:
|
||||
all_available_groups = Groups.get_all_groups()
|
||||
all_available_groups = Groups.get_all_groups(db=db)
|
||||
log.debug("Refreshed list of all available groups after creation.")
|
||||
|
||||
log.debug(f"Oauth Groups claim: {oauth_claim}")
|
||||
|
|
@ -1227,7 +1288,7 @@ class OAuthManager:
|
|||
log.debug(
|
||||
f"Removing user from group {group_model.name} as it is no longer in their oauth groups"
|
||||
)
|
||||
Groups.remove_users_from_group(group_model.id, [user.id])
|
||||
Groups.remove_users_from_group(group_model.id, [user.id], db=db)
|
||||
|
||||
# In case a group is created, but perms are never assigned to the group by hitting "save"
|
||||
group_permissions = group_model.permissions
|
||||
|
|
@ -1242,6 +1303,7 @@ class OAuthManager:
|
|||
permissions=group_permissions,
|
||||
),
|
||||
overwrite=False,
|
||||
db=db,
|
||||
)
|
||||
|
||||
# Add user to new groups
|
||||
|
|
@ -1257,7 +1319,7 @@ class OAuthManager:
|
|||
f"Adding user to group {group_model.name} as it was found in their oauth groups"
|
||||
)
|
||||
|
||||
Groups.add_users_to_group(group_model.id, [user.id])
|
||||
Groups.add_users_to_group(group_model.id, [user.id], db=db)
|
||||
|
||||
# In case a group is created, but perms are never assigned to the group by hitting "save"
|
||||
group_permissions = group_model.permissions
|
||||
|
|
@ -1272,6 +1334,7 @@ class OAuthManager:
|
|||
permissions=group_permissions,
|
||||
),
|
||||
overwrite=False,
|
||||
db=db,
|
||||
)
|
||||
|
||||
async def _process_picture_url(
|
||||
|
|
@ -1336,7 +1399,7 @@ class OAuthManager:
|
|||
|
||||
return await client.authorize_redirect(request, redirect_uri, **kwargs)
|
||||
|
||||
async def handle_callback(self, request, provider, response):
|
||||
async def handle_callback(self, request, provider, response, db=None):
|
||||
if provider not in OAUTH_PROVIDERS:
|
||||
raise HTTPException(404)
|
||||
|
||||
|
|
@ -1461,20 +1524,20 @@ class OAuthManager:
|
|||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||
|
||||
# Check if the user exists
|
||||
user = Users.get_user_by_oauth_sub(provider, sub)
|
||||
user = Users.get_user_by_oauth_sub(provider, sub, db=db)
|
||||
if not user:
|
||||
# If the user does not exist, check if merging is enabled
|
||||
if auth_manager_config.OAUTH_MERGE_ACCOUNTS_BY_EMAIL:
|
||||
# Check if the user exists by email
|
||||
user = Users.get_user_by_email(email)
|
||||
user = Users.get_user_by_email(email, db=db)
|
||||
if user:
|
||||
# Update the user with the new oauth sub
|
||||
Users.update_user_oauth_by_id(user.id, provider, sub)
|
||||
Users.update_user_oauth_by_id(user.id, provider, sub, db=db)
|
||||
|
||||
if user:
|
||||
determined_role = self.get_user_role(user, user_data)
|
||||
if user.role != determined_role:
|
||||
Users.update_user_role_by_id(user.id, determined_role)
|
||||
Users.update_user_role_by_id(user.id, determined_role, db=db)
|
||||
# Update the user object in memory as well,
|
||||
# to avoid problems with the ENABLE_OAUTH_GROUP_MANAGEMENT check below
|
||||
user.role = determined_role
|
||||
|
|
@ -1491,14 +1554,14 @@ class OAuthManager:
|
|||
)
|
||||
if processed_picture_url != user.profile_image_url:
|
||||
Users.update_user_profile_image_url_by_id(
|
||||
user.id, processed_picture_url
|
||||
user.id, processed_picture_url, db=db
|
||||
)
|
||||
log.debug(f"Updated profile picture for user {user.email}")
|
||||
else:
|
||||
# If the user does not exist, check if signups are enabled
|
||||
if auth_manager_config.ENABLE_OAUTH_SIGNUP:
|
||||
# Check if an existing user with the same email already exists
|
||||
existing_user = Users.get_user_by_email(email)
|
||||
existing_user = Users.get_user_by_email(email, db=db)
|
||||
if existing_user:
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
|
||||
|
||||
|
|
@ -1529,6 +1592,7 @@ class OAuthManager:
|
|||
profile_image_url=picture_url,
|
||||
role=self.get_user_role(None, user_data),
|
||||
oauth=oauth_data,
|
||||
db=db,
|
||||
)
|
||||
|
||||
if auth_manager_config.WEBHOOK_URL:
|
||||
|
|
@ -1544,8 +1608,7 @@ class OAuthManager:
|
|||
)
|
||||
|
||||
apply_default_group_assignment(
|
||||
request.app.state.config.DEFAULT_GROUP_ID,
|
||||
user.id,
|
||||
request.app.state.config.DEFAULT_GROUP_ID, user.id, db=db
|
||||
)
|
||||
|
||||
else:
|
||||
|
|
@ -1566,6 +1629,7 @@ class OAuthManager:
|
|||
user=user,
|
||||
user_data=user_data,
|
||||
default_permissions=request.app.state.config.USER_PERMISSIONS,
|
||||
db=db,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -1616,15 +1680,16 @@ class OAuthManager:
|
|||
token["expires_at"] = datetime.now().timestamp() + token["expires_in"]
|
||||
|
||||
# Clean up any existing sessions for this user/provider first
|
||||
sessions = OAuthSessions.get_sessions_by_user_id(user.id)
|
||||
sessions = OAuthSessions.get_sessions_by_user_id(user.id, db=db)
|
||||
for session in sessions:
|
||||
if session.provider == provider:
|
||||
OAuthSessions.delete_session_by_id(session.id)
|
||||
OAuthSessions.delete_session_by_id(session.id, db=db)
|
||||
|
||||
session = OAuthSessions.create_session(
|
||||
user_id=user.id,
|
||||
provider=provider,
|
||||
token=token,
|
||||
db=db,
|
||||
)
|
||||
|
||||
response.set_cookie(
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import types
|
|||
import tempfile
|
||||
import logging
|
||||
|
||||
from open_webui.env import PIP_OPTIONS, PIP_PACKAGE_INDEX_OPTIONS
|
||||
from open_webui.env import PIP_OPTIONS, PIP_PACKAGE_INDEX_OPTIONS, OFFLINE_MODE
|
||||
from open_webui.models.functions import Functions
|
||||
from open_webui.models.tools import Tools
|
||||
|
||||
|
|
@ -264,6 +264,10 @@ def get_function_module_from_cache(request, function_id, load_from_db=True):
|
|||
|
||||
|
||||
def install_frontmatter_requirements(requirements: str):
|
||||
if OFFLINE_MODE:
|
||||
log.info("Offline mode enabled, skipping installation of requirements.")
|
||||
return
|
||||
|
||||
if requirements:
|
||||
try:
|
||||
req_list = [req.strip() for req in requirements.split(",")]
|
||||
|
|
|
|||
|
|
@ -42,6 +42,38 @@ class SentinelRedisProxy:
|
|||
return orig_attr
|
||||
|
||||
if self._async_mode:
|
||||
if inspect.isasyncgenfunction(orig_attr):
|
||||
|
||||
def _wrapped_iter(*args, **kwargs):
|
||||
async def _iter():
|
||||
for i in range(REDIS_SENTINEL_MAX_RETRY_COUNT):
|
||||
try:
|
||||
method = getattr(self._master(), item)
|
||||
async for value in method(*args, **kwargs):
|
||||
yield value
|
||||
return
|
||||
except (
|
||||
redis.exceptions.ConnectionError,
|
||||
redis.exceptions.ReadOnlyError,
|
||||
) as e:
|
||||
if i < REDIS_SENTINEL_MAX_RETRY_COUNT - 1:
|
||||
log.debug(
|
||||
"Redis sentinel fail-over (%s). Retry %s/%s",
|
||||
type(e).__name__,
|
||||
i + 1,
|
||||
REDIS_SENTINEL_MAX_RETRY_COUNT,
|
||||
)
|
||||
continue
|
||||
log.error(
|
||||
"Redis operation failed after %s retries: %s",
|
||||
REDIS_SENTINEL_MAX_RETRY_COUNT,
|
||||
e,
|
||||
)
|
||||
raise e from e
|
||||
|
||||
return _iter()
|
||||
|
||||
return _wrapped_iter
|
||||
|
||||
async def _wrapped(*args, **kwargs):
|
||||
for i in range(REDIS_SENTINEL_MAX_RETRY_COUNT):
|
||||
|
|
|
|||
|
|
@ -37,12 +37,42 @@ from langchain_core.utils.function_calling import (
|
|||
from open_webui.utils.misc import is_string_allowed
|
||||
from open_webui.models.tools import Tools
|
||||
from open_webui.models.users import UserModel
|
||||
from open_webui.models.groups import Groups
|
||||
from open_webui.utils.plugin import load_tool_module_by_id
|
||||
from open_webui.utils.access_control import has_access
|
||||
from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL
|
||||
from open_webui.env import (
|
||||
AIOHTTP_CLIENT_TIMEOUT,
|
||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA,
|
||||
AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
|
||||
)
|
||||
from open_webui.tools.builtin import (
|
||||
search_web,
|
||||
fetch_url,
|
||||
generate_image,
|
||||
edit_image,
|
||||
search_memories,
|
||||
add_memory,
|
||||
replace_memory_content,
|
||||
get_current_timestamp,
|
||||
calculate_timestamp,
|
||||
search_notes,
|
||||
search_chats,
|
||||
search_channels,
|
||||
search_channel_messages,
|
||||
view_note,
|
||||
view_chat,
|
||||
view_channel_message,
|
||||
view_channel_thread,
|
||||
replace_note_content,
|
||||
write_note,
|
||||
list_knowledge_bases,
|
||||
search_knowledge_bases,
|
||||
query_knowledge_bases,
|
||||
search_knowledge_files,
|
||||
query_knowledge_files,
|
||||
view_knowledge_file,
|
||||
)
|
||||
|
||||
import copy
|
||||
|
||||
|
|
@ -104,15 +134,114 @@ def get_updated_tool_function(function: Callable, extra_params: dict):
|
|||
return function
|
||||
|
||||
|
||||
def has_tool_server_access(
|
||||
user: UserModel, server_connection: dict, user_group_ids: set = None
|
||||
) -> bool:
|
||||
"""Check if user has access to a tool server (MCP or OpenAPI)."""
|
||||
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
|
||||
return True
|
||||
|
||||
if user_group_ids is None:
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
|
||||
|
||||
access_control = server_connection.get("config", {}).get("access_control", None)
|
||||
return has_access(user.id, "read", access_control, user_group_ids)
|
||||
|
||||
|
||||
async def get_tools(
|
||||
request: Request, tool_ids: list[str], user: UserModel, extra_params: dict
|
||||
) -> dict[str, dict]:
|
||||
"""Load tools for the given tool_ids, checking access control."""
|
||||
tools_dict = {}
|
||||
|
||||
# Get user's group memberships for access control checks
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
|
||||
|
||||
for tool_id in tool_ids:
|
||||
tool = Tools.get_tool_by_id(tool_id)
|
||||
if tool is None:
|
||||
if tool:
|
||||
# Check access control for local tools
|
||||
if (
|
||||
not (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
and tool.user_id != user.id
|
||||
and not has_access(user.id, "read", tool.access_control, user_group_ids)
|
||||
):
|
||||
log.warning(f"Access denied to tool {tool_id} for user {user.id}")
|
||||
continue
|
||||
|
||||
module = request.app.state.TOOLS.get(tool_id, None)
|
||||
if module is None:
|
||||
module, _ = load_tool_module_by_id(tool_id)
|
||||
request.app.state.TOOLS[tool_id] = module
|
||||
|
||||
__user__ = {
|
||||
**extra_params["__user__"],
|
||||
}
|
||||
|
||||
# Set valves for the tool
|
||||
if hasattr(module, "valves") and hasattr(module, "Valves"):
|
||||
valves = Tools.get_tool_valves_by_id(tool_id) or {}
|
||||
module.valves = module.Valves(**valves)
|
||||
if hasattr(module, "UserValves"):
|
||||
__user__["valves"] = module.UserValves( # type: ignore
|
||||
**Tools.get_user_valves_by_id_and_user_id(tool_id, user.id)
|
||||
)
|
||||
|
||||
for spec in tool.specs:
|
||||
# TODO: Fix hack for OpenAI API
|
||||
# Some times breaks OpenAI but others don't. Leaving the comment
|
||||
for val in spec.get("parameters", {}).get("properties", {}).values():
|
||||
if val.get("type") == "str":
|
||||
val["type"] = "string"
|
||||
|
||||
# Remove internal reserved parameters (e.g. __id__, __user__)
|
||||
spec["parameters"]["properties"] = {
|
||||
key: val
|
||||
for key, val in spec["parameters"]["properties"].items()
|
||||
if not key.startswith("__")
|
||||
}
|
||||
|
||||
# convert to function that takes only model params and inserts custom params
|
||||
function_name = spec["name"]
|
||||
tool_function = getattr(module, function_name)
|
||||
callable = get_async_tool_function_and_apply_extra_params(
|
||||
tool_function,
|
||||
{
|
||||
**extra_params,
|
||||
"__id__": tool_id,
|
||||
"__user__": __user__,
|
||||
},
|
||||
)
|
||||
|
||||
# TODO: Support Pydantic models as parameters
|
||||
if callable.__doc__ and callable.__doc__.strip() != "":
|
||||
s = re.split(":(param|return)", callable.__doc__, 1)
|
||||
spec["description"] = s[0]
|
||||
else:
|
||||
spec["description"] = function_name
|
||||
|
||||
tool_dict = {
|
||||
"tool_id": tool_id,
|
||||
"callable": callable,
|
||||
"spec": spec,
|
||||
# Misc info
|
||||
"metadata": {
|
||||
"file_handler": hasattr(module, "file_handler")
|
||||
and module.file_handler,
|
||||
"citation": hasattr(module, "citation") and module.citation,
|
||||
},
|
||||
}
|
||||
|
||||
# Handle function name collisions
|
||||
while function_name in tools_dict:
|
||||
log.warning(
|
||||
f"Tool {function_name} already exists in another tools!"
|
||||
)
|
||||
# Prepend tool ID to function name
|
||||
function_name = f"{tool_id}_{function_name}"
|
||||
|
||||
tools_dict[function_name] = tool_dict
|
||||
else:
|
||||
if tool_id.startswith("server:"):
|
||||
splits = tool_id.split(":")
|
||||
|
||||
|
|
@ -147,6 +276,15 @@ async def get_tools(
|
|||
]
|
||||
)
|
||||
|
||||
# Check access control for tool server
|
||||
if not has_tool_server_access(
|
||||
user, tool_server_connection, user_group_ids
|
||||
):
|
||||
log.warning(
|
||||
f"Access denied to tool server {server_id} for user {user.id}"
|
||||
)
|
||||
continue
|
||||
|
||||
specs = tool_server_data.get("specs", [])
|
||||
function_name_filter_list = tool_server_connection.get(
|
||||
"config", {}
|
||||
|
|
@ -241,81 +379,116 @@ async def get_tools(
|
|||
else:
|
||||
continue
|
||||
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
module = request.app.state.TOOLS.get(tool_id, None)
|
||||
if module is None:
|
||||
module, _ = load_tool_module_by_id(tool_id)
|
||||
request.app.state.TOOLS[tool_id] = module
|
||||
return tools_dict
|
||||
|
||||
__user__ = {
|
||||
**extra_params["__user__"],
|
||||
}
|
||||
|
||||
# Set valves for the tool
|
||||
if hasattr(module, "valves") and hasattr(module, "Valves"):
|
||||
valves = Tools.get_tool_valves_by_id(tool_id) or {}
|
||||
module.valves = module.Valves(**valves)
|
||||
if hasattr(module, "UserValves"):
|
||||
__user__["valves"] = module.UserValves( # type: ignore
|
||||
**Tools.get_user_valves_by_id_and_user_id(tool_id, user.id)
|
||||
)
|
||||
def get_builtin_tools(
|
||||
request: Request, extra_params: dict, features: dict = None, model: dict = None
|
||||
) -> dict[str, dict]:
|
||||
"""
|
||||
Get built-in tools for native function calling.
|
||||
Only returns tools when BOTH the global config is enabled AND the model capability allows it.
|
||||
"""
|
||||
tools_dict = {}
|
||||
builtin_functions = []
|
||||
features = features or {}
|
||||
model = model or {}
|
||||
|
||||
for spec in tool.specs:
|
||||
# TODO: Fix hack for OpenAI API
|
||||
# Some times breaks OpenAI but others don't. Leaving the comment
|
||||
for val in spec.get("parameters", {}).get("properties", {}).values():
|
||||
if val.get("type") == "str":
|
||||
val["type"] = "string"
|
||||
# Helper to get model capabilities (defaults to True if not specified)
|
||||
def get_model_capability(name: str, default: bool = True) -> bool:
|
||||
return (
|
||||
model.get("info", {})
|
||||
.get("meta", {})
|
||||
.get("capabilities", {})
|
||||
.get(name, default)
|
||||
)
|
||||
|
||||
# Remove internal reserved parameters (e.g. __id__, __user__)
|
||||
spec["parameters"]["properties"] = {
|
||||
key: val
|
||||
for key, val in spec["parameters"]["properties"].items()
|
||||
if not key.startswith("__")
|
||||
}
|
||||
# Time utilities - always available for date calculations
|
||||
builtin_functions.extend([get_current_timestamp, calculate_timestamp])
|
||||
|
||||
# convert to function that takes only model params and inserts custom params
|
||||
function_name = spec["name"]
|
||||
tool_function = getattr(module, function_name)
|
||||
callable = get_async_tool_function_and_apply_extra_params(
|
||||
tool_function,
|
||||
{
|
||||
**extra_params,
|
||||
"__id__": tool_id,
|
||||
"__user__": __user__,
|
||||
},
|
||||
)
|
||||
# Knowledge base tools - conditional injection based on model knowledge
|
||||
# If model has attached knowledge (any type), only provide query_knowledge_files
|
||||
# Otherwise, provide all KB browsing tools
|
||||
model_knowledge = model.get("info", {}).get("meta", {}).get("knowledge", [])
|
||||
if model_knowledge:
|
||||
# Model has attached knowledge - only allow semantic search within it
|
||||
builtin_functions.append(query_knowledge_files)
|
||||
else:
|
||||
# No model knowledge - allow full KB browsing
|
||||
builtin_functions.extend(
|
||||
[
|
||||
list_knowledge_bases,
|
||||
search_knowledge_bases,
|
||||
query_knowledge_bases,
|
||||
search_knowledge_files,
|
||||
query_knowledge_files,
|
||||
view_knowledge_file,
|
||||
]
|
||||
)
|
||||
|
||||
# TODO: Support Pydantic models as parameters
|
||||
if callable.__doc__ and callable.__doc__.strip() != "":
|
||||
s = re.split(":(param|return)", callable.__doc__, 1)
|
||||
spec["description"] = s[0]
|
||||
else:
|
||||
spec["description"] = function_name
|
||||
# Chats tools - search and fetch user's chat history
|
||||
builtin_functions.extend([search_chats, view_chat])
|
||||
|
||||
tool_dict = {
|
||||
"tool_id": tool_id,
|
||||
"callable": callable,
|
||||
"spec": spec,
|
||||
# Misc info
|
||||
"metadata": {
|
||||
"file_handler": hasattr(module, "file_handler")
|
||||
and module.file_handler,
|
||||
"citation": hasattr(module, "citation") and module.citation,
|
||||
},
|
||||
}
|
||||
# Add memory tools if enabled for this chat
|
||||
if features.get("memory"):
|
||||
builtin_functions.extend([search_memories, add_memory, replace_memory_content])
|
||||
|
||||
# Handle function name collisions
|
||||
while function_name in tools_dict:
|
||||
log.warning(
|
||||
f"Tool {function_name} already exists in another tools!"
|
||||
)
|
||||
# Prepend tool ID to function name
|
||||
function_name = f"{tool_id}_{function_name}"
|
||||
# Add web search tools if enabled globally AND model has web_search capability
|
||||
if getattr(
|
||||
request.app.state.config, "ENABLE_WEB_SEARCH", False
|
||||
) and get_model_capability("web_search"):
|
||||
builtin_functions.extend([search_web, fetch_url])
|
||||
|
||||
tools_dict[function_name] = tool_dict
|
||||
# Add image generation/edit tools if enabled globally AND model has image_generation capability
|
||||
if getattr(
|
||||
request.app.state.config, "ENABLE_IMAGE_GENERATION", False
|
||||
) and get_model_capability("image_generation"):
|
||||
builtin_functions.append(generate_image)
|
||||
if getattr(
|
||||
request.app.state.config, "ENABLE_IMAGE_EDIT", False
|
||||
) and get_model_capability("image_generation"):
|
||||
builtin_functions.append(edit_image)
|
||||
|
||||
# Notes tools - search, view, create, and update user's notes (if notes enabled globally)
|
||||
if getattr(request.app.state.config, "ENABLE_NOTES", False):
|
||||
builtin_functions.extend(
|
||||
[search_notes, view_note, write_note, replace_note_content]
|
||||
)
|
||||
|
||||
# Channels tools - search channels and messages (if channels enabled globally)
|
||||
if getattr(request.app.state.config, "ENABLE_CHANNELS", False):
|
||||
builtin_functions.extend(
|
||||
[
|
||||
search_channels,
|
||||
search_channel_messages,
|
||||
view_channel_thread,
|
||||
view_channel_message,
|
||||
]
|
||||
)
|
||||
|
||||
for func in builtin_functions:
|
||||
callable = get_async_tool_function_and_apply_extra_params(
|
||||
func,
|
||||
{
|
||||
"__request__": request,
|
||||
"__user__": extra_params.get("__user__", {}),
|
||||
"__event_emitter__": extra_params.get("__event_emitter__"),
|
||||
"__chat_id__": extra_params.get("__chat_id__"),
|
||||
"__message_id__": extra_params.get("__message_id__"),
|
||||
"__model_knowledge__": model_knowledge,
|
||||
},
|
||||
)
|
||||
|
||||
# Generate spec from function
|
||||
pydantic_model = convert_function_to_pydantic_model(func)
|
||||
spec = convert_pydantic_model_to_openai_function_spec(pydantic_model)
|
||||
|
||||
tools_dict[func.__name__] = {
|
||||
"tool_id": f"builtin:{func.__name__}",
|
||||
"callable": callable,
|
||||
"spec": spec,
|
||||
"type": "builtin",
|
||||
}
|
||||
|
||||
return tools_dict
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import logging
|
|||
import aiohttp
|
||||
|
||||
from open_webui.config import WEBUI_FAVICON_URL
|
||||
from open_webui.env import VERSION
|
||||
from open_webui.env import AIOHTTP_CLIENT_TIMEOUT, VERSION
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -50,7 +50,9 @@ async def post_webhook(name: str, url: str, message: str, event_data: dict) -> b
|
|||
payload = {**event_data}
|
||||
|
||||
log.debug(f"payload: {payload}")
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with aiohttp.ClientSession(
|
||||
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
||||
) as session:
|
||||
async with session.post(url, json=payload) as r:
|
||||
r_text = await r.text()
|
||||
r.raise_for_status()
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
# Minimal requirements for backend to run
|
||||
# WIP: use this as a reference to build a minimal docker image
|
||||
|
||||
fastapi==0.126.0
|
||||
uvicorn[standard]==0.37.0
|
||||
fastapi==0.128.0
|
||||
uvicorn[standard]==0.40.0
|
||||
pydantic==2.12.5
|
||||
python-multipart==0.0.21
|
||||
itsdangerous==2.2.0
|
||||
|
||||
python-socketio==5.15.1
|
||||
python-socketio==5.16.0
|
||||
python-jose==3.5.0
|
||||
cryptography
|
||||
bcrypt==5.0.0
|
||||
argon2-cffi==25.1.0
|
||||
PyJWT[crypto]==2.10.1
|
||||
authlib==1.6.5
|
||||
authlib==1.6.6
|
||||
|
||||
requests==2.32.5
|
||||
aiohttp==3.13.2
|
||||
|
|
@ -32,7 +32,7 @@ peewee-migrate==1.14.3
|
|||
pycrdt==0.12.44
|
||||
redis
|
||||
|
||||
APScheduler==3.11.1
|
||||
APScheduler==3.11.2
|
||||
RestrictedPython==8.1
|
||||
|
||||
loguru==0.7.3
|
||||
|
|
@ -41,11 +41,14 @@ asgiref==3.11.0
|
|||
mcp==1.25.0
|
||||
openai
|
||||
|
||||
langchain==0.3.27
|
||||
langchain-community==0.3.29
|
||||
langchain==1.2.0
|
||||
langchain-community==0.4.1
|
||||
langchain-classic==1.0.1
|
||||
langchain-text-splitters==1.1.0
|
||||
|
||||
fake-useragent==2.2.0
|
||||
|
||||
chromadb==1.3.7
|
||||
chromadb==1.4.0
|
||||
black==25.12.0
|
||||
pydub
|
||||
chardet==5.2.0
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
fastapi==0.126.0
|
||||
uvicorn[standard]==0.37.0
|
||||
fastapi==0.128.0
|
||||
uvicorn[standard]==0.40.0
|
||||
pydantic==2.12.5
|
||||
python-multipart==0.0.21
|
||||
itsdangerous==2.2.0
|
||||
|
||||
python-socketio==5.15.1
|
||||
python-socketio==5.16.0
|
||||
python-jose==3.5.0
|
||||
cryptography
|
||||
bcrypt==5.0.0
|
||||
|
|
@ -30,7 +30,7 @@ peewee-migrate==1.14.3
|
|||
pycrdt==0.12.44
|
||||
redis
|
||||
|
||||
APScheduler==3.11.1
|
||||
APScheduler==3.11.2
|
||||
RestrictedPython==8.1
|
||||
|
||||
loguru==0.7.3
|
||||
|
|
@ -43,16 +43,15 @@ mcp==1.25.0
|
|||
openai
|
||||
anthropic
|
||||
google-genai==1.56.0
|
||||
google-generativeai==0.8.6
|
||||
|
||||
langchain==1.2.0
|
||||
langchain-community==0.4.1
|
||||
langchain-classic==1.0.0
|
||||
langchain-classic==1.0.1
|
||||
langchain-text-splitters==1.1.0
|
||||
|
||||
fake-useragent==2.2.0
|
||||
chromadb==1.3.7
|
||||
weaviate-client==4.19.0
|
||||
chromadb==1.4.0
|
||||
weaviate-client==4.19.2
|
||||
opensearch-py==3.1.0
|
||||
|
||||
transformers==4.57.3
|
||||
|
|
@ -65,10 +64,10 @@ ftfy==6.3.1
|
|||
chardet==5.2.0
|
||||
pypdf==6.5.0
|
||||
fpdf2==2.8.5
|
||||
pymdown-extensions==10.19.1
|
||||
pymdown-extensions==10.20
|
||||
docx2txt==0.9
|
||||
python-pptx==1.0.2
|
||||
unstructured==0.18.21
|
||||
unstructured==0.18.24
|
||||
msoffcrypto-tool==5.4.2
|
||||
nltk==3.9.2
|
||||
Markdown==3.10
|
||||
|
|
@ -82,7 +81,7 @@ psutil
|
|||
sentencepiece
|
||||
soundfile==0.13.1
|
||||
|
||||
pillow==12.0.0
|
||||
pillow==12.1.0
|
||||
opencv-python-headless==4.12.0.88
|
||||
rapidocr-onnxruntime==1.4.4
|
||||
rank-bm25==0.2.2
|
||||
|
|
@ -116,12 +115,12 @@ psycopg2-binary==2.9.11
|
|||
pgvector==0.4.2
|
||||
|
||||
PyMySQL==1.1.2
|
||||
boto3==1.42.14
|
||||
boto3==1.42.21
|
||||
|
||||
pymilvus==2.6.5
|
||||
pymilvus==2.6.6
|
||||
qdrant-client==1.16.2
|
||||
playwright==1.57.0 # Caution: version must match docker-compose.playwright.yaml - Update the docker-compose.yaml if necessary
|
||||
elasticsearch==9.2.0
|
||||
elasticsearch==9.2.1
|
||||
pinecone==6.0.2
|
||||
oracledb==3.4.1
|
||||
|
||||
|
|
|
|||
100
package-lock.json
generated
100
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.6.43",
|
||||
"version": "0.7.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "open-webui",
|
||||
"version": "0.6.43",
|
||||
"version": "0.7.0",
|
||||
"dependencies": {
|
||||
"@azure/msal-browser": "^4.5.0",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
"i18next-resources-to-backend": "^1.2.0",
|
||||
"idb": "^7.1.1",
|
||||
"js-sha256": "^0.10.1",
|
||||
"jspdf": "^3.0.0",
|
||||
"jspdf": "^4.0.0",
|
||||
"katex": "^0.16.22",
|
||||
"kokoro-js": "^1.1.1",
|
||||
"leaflet": "^1.9.4",
|
||||
|
|
@ -99,6 +99,7 @@
|
|||
"vite-plugin-static-copy": "^2.2.0",
|
||||
"xlsx": "^0.18.5",
|
||||
"y-prosemirror": "^1.3.7",
|
||||
"y-protocols": "^1.0.7",
|
||||
"yaml": "^2.7.1",
|
||||
"yjs": "^13.6.27"
|
||||
},
|
||||
|
|
@ -128,7 +129,7 @@
|
|||
"tailwindcss": "^4.0.0",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.14",
|
||||
"vite": "^5.4.21",
|
||||
"vitest": "^1.6.1"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -217,13 +218,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
|
||||
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
|
|
@ -4218,6 +4216,12 @@
|
|||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pako": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
|
||||
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/raf": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||
|
|
@ -4900,18 +4904,6 @@
|
|||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/atob": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"bin": {
|
||||
"atob": "bin/atob.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-sign2": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
||||
|
|
@ -5183,18 +5175,6 @@
|
|||
"node": "10.* || >= 12.*"
|
||||
}
|
||||
},
|
||||
"node_modules/btoa": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
|
||||
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"bin": {
|
||||
"btoa": "bin/btoa.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
|
|
@ -7649,6 +7629,23 @@
|
|||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-png": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
|
||||
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/pako": "^2.0.3",
|
||||
"iobuffer": "^5.3.2",
|
||||
"pako": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-png/node_modules/pako": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
||||
|
|
@ -8649,6 +8646,12 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/iobuffer": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||
|
|
@ -9014,14 +9017,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/jspdf": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz",
|
||||
"integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.0.0.tgz",
|
||||
"integrity": "sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.7",
|
||||
"atob": "^2.1.2",
|
||||
"btoa": "^1.2.1",
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"fast-png": "^6.2.0",
|
||||
"fflate": "^0.8.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
|
@ -11382,11 +11384,6 @@
|
|||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||
},
|
||||
"node_modules/remove-trailing-separator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
|
||||
|
|
@ -13761,9 +13758,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.19",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
|
||||
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
|
||||
"version": "5.4.21",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
|
|
@ -14865,11 +14862,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/y-protocols": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz",
|
||||
"integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.7.tgz",
|
||||
"integrity": "sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lib0": "^0.2.85"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.6.43",
|
||||
"version": "0.7.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
"tailwindcss": "^4.0.0",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.14",
|
||||
"vite": "^5.4.21",
|
||||
"vitest": "^1.6.1"
|
||||
},
|
||||
"type": "module",
|
||||
|
|
@ -106,7 +106,7 @@
|
|||
"i18next-resources-to-backend": "^1.2.0",
|
||||
"idb": "^7.1.1",
|
||||
"js-sha256": "^0.10.1",
|
||||
"jspdf": "^3.0.0",
|
||||
"jspdf": "^4.0.0",
|
||||
"katex": "^0.16.22",
|
||||
"kokoro-js": "^1.1.1",
|
||||
"leaflet": "^1.9.4",
|
||||
|
|
@ -143,6 +143,7 @@
|
|||
"vite-plugin-static-copy": "^2.2.0",
|
||||
"xlsx": "^0.18.5",
|
||||
"y-prosemirror": "^1.3.7",
|
||||
"y-protocols": "^1.0.7",
|
||||
"yaml": "^2.7.1",
|
||||
"yjs": "^13.6.27"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ authors = [
|
|||
]
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = [
|
||||
"fastapi==0.126.0",
|
||||
"uvicorn[standard]==0.37.0",
|
||||
"fastapi==0.128.0",
|
||||
"uvicorn[standard]==0.40.0",
|
||||
"pydantic==2.12.5",
|
||||
"python-multipart==0.0.21",
|
||||
"itsdangerous==2.2.0",
|
||||
|
||||
"python-socketio==5.15.1",
|
||||
"python-socketio==5.16.0",
|
||||
"python-jose==3.5.0",
|
||||
"cryptography",
|
||||
"bcrypt==5.0.0",
|
||||
|
|
@ -38,7 +38,7 @@ dependencies = [
|
|||
"pycrdt==0.12.44",
|
||||
"redis",
|
||||
|
||||
"APScheduler==3.11.1",
|
||||
"APScheduler==3.11.2",
|
||||
"RestrictedPython==8.1",
|
||||
|
||||
"loguru==0.7.3",
|
||||
|
|
@ -50,18 +50,17 @@ dependencies = [
|
|||
"openai",
|
||||
"anthropic",
|
||||
"google-genai==1.56.0",
|
||||
"google-generativeai==0.8.6",
|
||||
|
||||
"langchain==1.2.0",
|
||||
"langchain-community==0.4.1",
|
||||
"langchain-classic==1.0.0",
|
||||
"langchain-classic==1.0.1",
|
||||
"langchain-text-splitters==1.1.0",
|
||||
|
||||
"fake-useragent==2.2.0",
|
||||
"chromadb==1.3.7",
|
||||
"chromadb==1.4.0",
|
||||
"opensearch-py==3.1.0",
|
||||
"PyMySQL==1.1.2",
|
||||
"boto3==1.42.14",
|
||||
"boto3==1.42.21",
|
||||
|
||||
"transformers==4.57.3",
|
||||
"sentence-transformers==5.2.0",
|
||||
|
|
@ -73,10 +72,10 @@ dependencies = [
|
|||
"chardet==5.2.0",
|
||||
"pypdf==6.5.0",
|
||||
"fpdf2==2.8.5",
|
||||
"pymdown-extensions==10.19.1",
|
||||
"pymdown-extensions==10.20",
|
||||
"docx2txt==0.9",
|
||||
"python-pptx==1.0.2",
|
||||
"unstructured==0.18.21",
|
||||
"unstructured==0.18.24",
|
||||
"msoffcrypto-tool==5.4.2",
|
||||
"nltk==3.9.2",
|
||||
"Markdown==3.10",
|
||||
|
|
@ -91,7 +90,7 @@ dependencies = [
|
|||
"soundfile==0.13.1",
|
||||
"azure-ai-documentintelligence==1.0.2",
|
||||
|
||||
"pillow==12.0.0",
|
||||
"pillow==12.1.0",
|
||||
"opencv-python-headless==4.12.0.88",
|
||||
"rapidocr-onnxruntime==1.4.4",
|
||||
"rank-bm25==0.2.2",
|
||||
|
|
@ -139,20 +138,20 @@ postgres = [
|
|||
|
||||
all = [
|
||||
"pymongo",
|
||||
"psycopg2-binary==2.9.9",
|
||||
"pgvector==0.4.0",
|
||||
"psycopg2-binary==2.9.11",
|
||||
"pgvector==0.4.2",
|
||||
"moto[s3]>=5.0.26",
|
||||
"gcp-storage-emulator>=2024.8.3",
|
||||
"docker~=7.1.0",
|
||||
"pytest~=8.3.2",
|
||||
"pytest-docker~=3.2.5",
|
||||
"playwright==1.57.0", # Caution: version must match docker-compose.playwright.yaml - Update the docker-compose.yaml if necessary
|
||||
"elasticsearch==9.2.0",
|
||||
"elasticsearch==9.2.1",
|
||||
|
||||
"qdrant-client==1.16.2",
|
||||
"pymilvus==2.6.4",
|
||||
"weaviate-client==4.19.0",
|
||||
"pymilvus==2.6.5",
|
||||
|
||||
"weaviate-client==4.19.2",
|
||||
"pymilvus==2.6.6",
|
||||
"pinecone==6.0.2",
|
||||
"oracledb==3.4.1",
|
||||
"colbert-ai==0.2.22",
|
||||
|
|
|
|||
|
|
@ -628,7 +628,7 @@ input[type='number'] {
|
|||
|
||||
/* Table styling for tiptap editors */
|
||||
.tiptap table {
|
||||
@apply w-full text-sm text-left text-gray-500 dark:text-gray-400 max-w-full;
|
||||
@apply w-full text-sm text-start text-gray-500 dark:text-gray-400 max-w-full;
|
||||
}
|
||||
|
||||
.tiptap thead {
|
||||
|
|
@ -641,7 +641,7 @@ input[type='number'] {
|
|||
}
|
||||
|
||||
.tiptap th {
|
||||
@apply cursor-pointer text-left text-xs text-gray-700 dark:text-gray-400 font-semibold uppercase bg-gray-50 dark:bg-gray-850;
|
||||
@apply cursor-pointer text-start text-xs text-gray-700 dark:text-gray-400 font-semibold uppercase bg-gray-50 dark:bg-gray-850;
|
||||
}
|
||||
|
||||
.tiptap td {
|
||||
|
|
|
|||
|
|
@ -423,6 +423,19 @@ export const updateUserProfile = async (token: string, profile: object) => {
|
|||
return res;
|
||||
};
|
||||
|
||||
export const updateUserTimezone = async (token: string, timezone: string) => {
|
||||
await fetch(`${WEBUI_API_BASE_URL}/auths/update/timezone`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify({ timezone })
|
||||
}).catch((err) => {
|
||||
console.error('Failed to update timezone:', err);
|
||||
});
|
||||
};
|
||||
|
||||
export const updateUserPassword = async (token: string, password: string, newPassword: string) => {
|
||||
let error = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -763,3 +763,155 @@ export const deleteMessage = async (token: string = '', channel_id: string, mess
|
|||
|
||||
return res;
|
||||
};
|
||||
|
||||
// Webhook API functions
|
||||
|
||||
type WebhookForm = {
|
||||
name: string;
|
||||
profile_image_url?: string;
|
||||
};
|
||||
|
||||
export const getChannelWebhooks = async (token: string = '', channel_id: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/webhooks`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const createChannelWebhook = async (
|
||||
token: string = '',
|
||||
channel_id: string,
|
||||
formData: WebhookForm
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/webhooks/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ ...formData })
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateChannelWebhook = async (
|
||||
token: string = '',
|
||||
channel_id: string,
|
||||
webhook_id: string,
|
||||
formData: WebhookForm
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(
|
||||
`${WEBUI_API_BASE_URL}/channels/${channel_id}/webhooks/${webhook_id}/update`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ ...formData })
|
||||
}
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const deleteChannelWebhook = async (
|
||||
token: string = '',
|
||||
channel_id: string,
|
||||
webhook_id: string
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(
|
||||
`${WEBUI_API_BASE_URL}/channels/${channel_id}/webhooks/${webhook_id}/delete`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -142,6 +142,10 @@ export const getChatList = async (
|
|||
throw error;
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return res.map((chat) => ({
|
||||
...chat,
|
||||
time_range: getTimeRange(chat.updated_at)
|
||||
|
|
@ -1166,3 +1170,103 @@ export const archiveAllChats = async (token: string) => {
|
|||
|
||||
return res;
|
||||
};
|
||||
export const exportChatStats = async (token: string, page: number = 1, params: object = {}) => {
|
||||
let error = null;
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('page', `${page}`);
|
||||
|
||||
if (params) {
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
searchParams.append(key, `${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/stats/export?${searchParams.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const exportSingleChatStats = async (token: string, chatId: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/stats/export/${chatId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const downloadChatStats = async (
|
||||
token: string = '',
|
||||
updated_at: number | null = null
|
||||
): Promise<[Response | null, AbortController]> => {
|
||||
const controller = new AbortController();
|
||||
let error = null;
|
||||
|
||||
let url = `${WEBUI_API_BASE_URL}/chats/stats/export?stream=true`;
|
||||
if (updated_at) url += `&updated_at=${updated_at}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
error = err;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return [res, controller];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -93,6 +93,74 @@ export const getAllFeedbacks = async (token: string = '') => {
|
|||
return res;
|
||||
};
|
||||
|
||||
export const getLeaderboard = async (token: string = '', query: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
if (query) searchParams.append('query', query);
|
||||
|
||||
const res = await fetch(
|
||||
`${WEBUI_API_BASE_URL}/evaluations/leaderboard?${searchParams.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getModelHistory = async (token: string = '', modelId: string, days: number = 30) => {
|
||||
let error = null;
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('days', days.toString());
|
||||
|
||||
const res = await fetch(
|
||||
`${WEBUI_API_BASE_URL}/evaluations/leaderboard/${encodeURIComponent(modelId)}/history?${searchParams.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getFeedbackItems = async (token: string = '', orderBy, direction, page) => {
|
||||
let error = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ export const getFileContentById = async (id: string) => {
|
|||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return await res.blob();
|
||||
return await res.arrayBuffer();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue