diff --git a/.github/dependabot.yml b/.github/dependabot.yml index af0a8ed0ee..ed93957ea4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,12 +1,26 @@ version: 2 updates: + - package-ecosystem: uv + directory: '/' + schedule: + interval: monthly + target-branch: 'dev' + - package-ecosystem: pip directory: '/backend' schedule: interval: monthly target-branch: 'dev' + + - package-ecosystem: npm + directory: '/' + schedule: + interval: monthly + target-branch: 'dev' + - package-ecosystem: 'github-actions' directory: '/' schedule: # Check for updates to GitHub Actions every week interval: monthly + target-branch: 'dev' diff --git a/.github/workflows/format-backend.yaml b/.github/workflows/format-backend.yaml index 4458766975..1bcdd92c1d 100644 --- a/.github/workflows/format-backend.yaml +++ b/.github/workflows/format-backend.yaml @@ -5,10 +5,18 @@ on: branches: - main - dev + paths: + - 'backend/**' + - 'pyproject.toml' + - 'uv.lock' pull_request: branches: - main - dev + paths: + - 'backend/**' + - 'pyproject.toml' + - 'uv.lock' jobs: build: @@ -17,7 +25,9 @@ jobs: strategy: matrix: - python-version: [3.11] + python-version: + - 3.11.x + - 3.12.x steps: - uses: actions/checkout@v4 @@ -25,7 +35,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} + python-version: '${{ matrix.python-version }}' - name: Install dependencies run: | diff --git a/.github/workflows/format-build-frontend.yaml b/.github/workflows/format-build-frontend.yaml index 53d3aaa5ec..9a007581ff 100644 --- a/.github/workflows/format-build-frontend.yaml +++ b/.github/workflows/format-build-frontend.yaml @@ -5,10 +5,18 @@ on: branches: - main - dev + paths-ignore: + - 'backend/**' + - 'pyproject.toml' + - 'uv.lock' pull_request: branches: - main - dev + paths-ignore: + - 'backend/**' + - 'pyproject.toml' + - 'uv.lock' jobs: build: @@ -21,7 +29,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '22' # Or specify any other version you want to use + node-version: '22' - name: Install Dependencies run: npm install diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aaa79292c..a11c2848ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ 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.6.2] - 2025-04-06 + +### Added + +- 🌍 **Improved Global Language Support**: Expanded and refined translations across multiple languages to enhance clarity and consistency for international users. + +### Fixed + +- 🛠️ **Accurate Tool Descriptions from OpenAPI Servers**: External tools now use full endpoint descriptions instead of summaries when generating tool specifications—helping AI models understand tool purpose more precisely and choose the right tool more accurately in tool workflows. +- 🔧 **Precise Web Results Source Attribution**: Fixed a key issue where all web search results showed the same source ID—now each result gets its correct and distinct source, ensuring accurate citations and traceability. +- 🔍 **Clean Web Search Retrieval**: Web search now retains only results from URLs where real content was successfully fetched—improving accuracy and removing empty or broken links from citations. +- 🎵 **Audio File Upload Response Restored**: Resolved an issue where uploading audio files did not return valid responses, restoring smooth file handling for transcription and audio-based workflows. + +### Changed + +- 🧰 **General Backend Refactoring**: Multiple behind-the-scenes improvements streamline backend performance, reduce complexity, and ensure a more stable, maintainable system overall—making everything smoother without changing your workflow. + ## [0.6.1] - 2025-04-05 ### Added diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index c9ca059c22..5c5d86eb16 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1053,6 +1053,7 @@ async def chat_completion( model_item = form_data.pop("model_item", {}) tasks = form_data.pop("background_tasks", None) + metadata = {} try: if not model_item.get("direct", False): model_id = form_data.get("model", None) @@ -1108,13 +1109,15 @@ async def chat_completion( except Exception as e: log.debug(f"Error processing chat payload: {e}") - Chats.upsert_message_to_chat_by_id_and_message_id( - metadata["chat_id"], - metadata["message_id"], - { - "error": {"content": str(e)}, - }, - ) + if metadata.get("chat_id") and metadata.get("message_id"): + # Update the chat message with the error + Chats.upsert_message_to_chat_by_id_and_message_id( + metadata["chat_id"], + metadata["message_id"], + { + "error": {"content": str(e)}, + }, + ) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index f2d2c61de5..12d48f8690 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -357,7 +357,7 @@ def get_embedding_function( ): if embedding_engine == "": return lambda query, prefix=None, user=None: embedding_function.encode( - query, prompt=prefix if prefix else None + query, **({"prompt": prefix} if prefix else {}) ).tolist() elif embedding_engine in ["ollama", "openai"]: func = lambda query, prefix=None, user=None: generate_embeddings( diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index 22e1269e37..c30366545e 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -122,6 +122,7 @@ def upload_file( ]: file_path = Storage.get_file(file_path) result = transcribe(request, file_path) + process_file( request, ProcessFileForm(file_id=id, content=result.get("text", "")), @@ -129,7 +130,8 @@ def upload_file( ) elif file.content_type not in ["image/png", "image/jpeg", "image/gif"]: process_file(request, ProcessFileForm(file_id=id), user=user) - file_item = Files.get_file_by_id(id=id) + + file_item = Files.get_file_by_id(id=id) except Exception as e: log.exception(e) log.error(f"Error processing file: {file_item.id}") @@ -162,11 +164,16 @@ def upload_file( @router.get("/", response_model=list[FileModelResponse]) -async def list_files(user=Depends(get_verified_user)): +async def list_files(user=Depends(get_verified_user), content: bool = Query(True)): if user.role == "admin": files = Files.get_files() else: files = Files.get_files_by_user_id(user.id) + + if not content: + for file in files: + del file.data["content"] + return files diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index 6f71e11d32..f31abd9ff0 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -150,8 +150,8 @@ def get_rf( device=DEVICE_TYPE, trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE, ) - except: - log.error("CrossEncoder error") + except Exception as e: + log.error(f"CrossEncoder: {e}") raise Exception(ERROR_MESSAGES.DEFAULT("CrossEncoder error")) return rf @@ -174,7 +174,7 @@ class ProcessUrlForm(CollectionNameForm): url: str -class SearchForm(CollectionNameForm): +class SearchForm(BaseModel): query: str @@ -958,7 +958,7 @@ def process_file( if form_data.content: # Update the content in the file - # Usage: /files/{file_id}/data/content/update + # Usage: /files/{file_id}/data/content/update, /files/ (audio file upload pipeline) try: # /files/{file_id}/data/content/update @@ -1464,12 +1464,6 @@ async def process_web_search( log.debug(f"web_results: {web_results}") try: - collection_name = form_data.collection_name - if collection_name == "" or collection_name is None: - collection_name = f"web-search-{calculate_sha256_string(form_data.query)}"[ - :63 - ] - urls = [result.link for result in web_results] loader = get_web_loader( urls, @@ -1478,6 +1472,9 @@ async def process_web_search( trust_env=request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV, ) docs = await loader.aload() + urls = [ + doc.metadata["source"] for doc in docs + ] # only keep URLs which could be retrieved if request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: return { @@ -1494,18 +1491,26 @@ async def process_web_search( "loaded_count": len(docs), } else: - await run_in_threadpool( - save_docs_to_vector_db, - request, - docs, - collection_name, - overwrite=True, - user=user, - ) + collection_names = [] + for doc_idx, doc in enumerate(docs): + collection_name = f"web-search-{calculate_sha256_string(form_data.query + '-' + urls[doc_idx])}"[ + :63 + ] + + collection_names.append(collection_name) + + await run_in_threadpool( + save_docs_to_vector_db, + request, + [doc], + collection_name, + overwrite=True, + user=user, + ) return { "status": True, - "collection_name": collection_name, + "collection_names": collection_names, "filenames": urls, "loaded_count": len(docs), } diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 62f43a7026..badae99065 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -399,24 +399,44 @@ async def chat_web_search_handler( all_results.append(results) files = form_data.get("files", []) - if results.get("collection_name"): - files.append( - { - "collection_name": results["collection_name"], - "name": searchQuery, - "type": "web_search", - "urls": results["filenames"], - } - ) + if results.get("collection_names"): + for col_idx, collection_name in enumerate( + results.get("collection_names") + ): + files.append( + { + "collection_name": collection_name, + "name": searchQuery, + "type": "web_search", + "urls": [results["filenames"][col_idx]], + } + ) elif results.get("docs"): - files.append( - { - "docs": results.get("docs", []), - "name": searchQuery, - "type": "web_search", - "urls": results["filenames"], - } - ) + # Invoked when bypass embedding and retrieval is set to True + docs = results["docs"] + + if len(docs) == len(results["filenames"]): + # the number of docs and filenames (urls) should be the same + for doc_idx, doc in enumerate(docs): + files.append( + { + "docs": [doc], + "name": searchQuery, + "type": "web_search", + "urls": [results["filenames"][doc_idx]], + } + ) + else: + # edge case when the number of docs and filenames (urls) are not the same + # this should not happen, but if it does, we will just append the docs + files.append( + { + "docs": results.get("docs", []), + "name": searchQuery, + "type": "web_search", + "urls": results["filenames"], + } + ) form_data["files"] = files except Exception as e: diff --git a/backend/requirements.txt b/backend/requirements.txt index dd7c859329..73c8550844 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -18,7 +18,7 @@ alembic==1.14.0 peewee==3.17.9 peewee-migrate==1.12.2 psycopg2-binary==2.9.9 -pgvector==0.3.5 +pgvector==0.4.0 PyMySQL==1.1.1 bcrypt==4.3.0 @@ -44,7 +44,7 @@ langchain==0.3.19 langchain-community==0.3.18 fake-useragent==2.1.0 -chromadb==0.6.2 +chromadb==0.6.3 pymilvus==2.5.0 qdrant-client~=1.12.0 opensearch-py==2.8.0 @@ -54,6 +54,7 @@ elasticsearch==8.17.1 transformers sentence-transformers==3.3.1 +accelerate colbert-ai==0.2.21 einops==0.8.1 @@ -92,7 +93,7 @@ authlib==1.4.1 black==25.1.0 langfuse==2.44.0 -youtube-transcript-api==0.6.3 +youtube-transcript-api==1.0.3 pytube==15.0.0 extract_msg @@ -112,7 +113,7 @@ pytest-docker~=3.1.1 googleapis-common-protos==1.63.2 google-cloud-storage==2.19.0 -azure-identity==1.20.0 +azure-identity==1.21.0 azure-storage-blob==12.24.1 @@ -123,14 +124,14 @@ ldap3==2.9.1 firecrawl-py==1.12.0 ## Trace -opentelemetry-api==1.30.0 -opentelemetry-sdk==1.30.0 -opentelemetry-exporter-otlp==1.30.0 -opentelemetry-instrumentation==0.51b0 -opentelemetry-instrumentation-fastapi==0.51b0 -opentelemetry-instrumentation-sqlalchemy==0.51b0 -opentelemetry-instrumentation-redis==0.51b0 -opentelemetry-instrumentation-requests==0.51b0 -opentelemetry-instrumentation-logging==0.51b0 -opentelemetry-instrumentation-httpx==0.51b0 -opentelemetry-instrumentation-aiohttp-client==0.51b0 \ No newline at end of file +opentelemetry-api==1.31.1 +opentelemetry-sdk==1.31.1 +opentelemetry-exporter-otlp==1.31.1 +opentelemetry-instrumentation==0.52b0 +opentelemetry-instrumentation-fastapi==0.52b0 +opentelemetry-instrumentation-sqlalchemy==0.52b0 +opentelemetry-instrumentation-redis==0.52b0 +opentelemetry-instrumentation-requests==0.52b0 +opentelemetry-instrumentation-logging==0.52b0 +opentelemetry-instrumentation-httpx==0.52b0 +opentelemetry-instrumentation-aiohttp-client==0.52b0 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d3de96f8e8..5e1aeb293c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.6.1", + "version": "0.6.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.6.1", + "version": "0.6.2", "dependencies": { "@azure/msal-browser": "^4.5.0", "@codemirror/lang-javascript": "^6.2.2", @@ -23,12 +23,13 @@ "@tiptap/extension-highlight": "^2.10.0", "@tiptap/extension-placeholder": "^2.10.0", "@tiptap/extension-typography": "^2.10.0", - "@tiptap/pm": "^2.10.0", + "@tiptap/pm": "^2.11.7", "@tiptap/starter-kit": "^2.10.0", "@xyflow/svelte": "^0.1.19", "async": "^3.2.5", "bits-ui": "^0.19.7", "codemirror": "^6.0.1", + "codemirror-lang-elixir": "^4.0.0", "codemirror-lang-hcl": "^0.0.0-beta.2", "crc-32": "^1.2.2", "dayjs": "^1.11.10", @@ -3042,9 +3043,9 @@ } }, "node_modules/@tiptap/pm": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.10.0.tgz", - "integrity": "sha512-ohshlWf4MlW6D3rQkNQnhmiQ2w4pwRoQcJmTPt8UJoIDGkeKmZh494fQp4Aeh80XuGd81SsCv//1HJeyaeHJYQ==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.11.7.tgz", + "integrity": "sha512-7gEEfz2Q6bYKXM07vzLUD0vqXFhC5geWRA6LCozTiLdVFDdHWiBrvb2rtkL5T7mfLq03zc1QhH7rI3F6VntOEA==", "license": "MIT", "dependencies": { "prosemirror-changeset": "^2.2.1", @@ -3061,10 +3062,10 @@ "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", - "prosemirror-tables": "^1.6.1", + "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", - "prosemirror-view": "^1.36.0" + "prosemirror-view": "^1.37.0" }, "funding": { "type": "github", @@ -4625,6 +4626,15 @@ "@codemirror/view": "^6.0.0" } }, + "node_modules/codemirror-lang-elixir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/codemirror-lang-elixir/-/codemirror-lang-elixir-4.0.0.tgz", + "integrity": "sha512-mzFesxo/t6KOxwnkqVd34R/q7yk+sMtHh6vUKGAvjwHmpL7bERHB+vQAsmU/nqrndkwVeJEHWGw/z/ybfdiudA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "lezer-elixir": "^1.0.0" + } + }, "node_modules/codemirror-lang-hcl": { "version": "0.0.0-beta.2", "resolved": "https://registry.npmjs.org/codemirror-lang-hcl/-/codemirror-lang-hcl-0.0.0-beta.2.tgz", @@ -7611,6 +7621,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lezer-elixir": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lezer-elixir/-/lezer-elixir-1.1.2.tgz", + "integrity": "sha512-K3yPMJcNhqCL6ugr5NkgOC1g37rcOM38XZezO9lBXy0LwWFd8zdWXfmRbY829vZVk0OGCQoI02yDWp9FF2OWZA==", + "dependencies": { + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.3.0" + } + }, "node_modules/lightningcss": { "version": "1.29.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz", @@ -9783,9 +9802,10 @@ } }, "node_modules/prosemirror-model": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.23.0.tgz", - "integrity": "sha512-Q/fgsgl/dlOAW9ILu4OOhYWQbc7TQd4BwKH/RwmUjyVf8682Be4zj3rOYdLnYEcGzyg8LL9Q5IWYKD8tdToreQ==", + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.0.tgz", + "integrity": "sha512-/8XUmxWf0pkj2BmtqZHYJipTBMHIdVjuvFzMvEoxrtyGNmfvdhBiRwYt/eFwy2wA9DtBW3RLqvZnjurEkHaFCw==", + "license": "MIT", "dependencies": { "orderedmap": "^2.0.0" } @@ -9819,16 +9839,16 @@ } }, "node_modules/prosemirror-tables": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.6.1.tgz", - "integrity": "sha512-p8WRJNA96jaNQjhJolmbxTzd6M4huRE5xQ8OxjvMhQUP0Nzpo4zz6TztEiwk6aoqGBhz9lxRWR1yRZLlpQN98w==", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.6.4.tgz", + "integrity": "sha512-TkDY3Gw52gRFRfRn2f4wJv5WOgAOXLJA2CQJYIJ5+kdFbfj3acR4JUW6LX2e1hiEBiUwvEhzH5a3cZ5YSztpIA==", "license": "MIT", "dependencies": { - "prosemirror-keymap": "^1.1.2", - "prosemirror-model": "^1.8.1", - "prosemirror-state": "^1.3.1", - "prosemirror-transform": "^1.2.1", - "prosemirror-view": "^1.13.3" + "prosemirror-keymap": "^1.2.2", + "prosemirror-model": "^1.24.1", + "prosemirror-state": "^1.4.3", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.37.2" } }, "node_modules/prosemirror-trailing-node": { @@ -9856,9 +9876,9 @@ } }, "node_modules/prosemirror-view": { - "version": "1.36.0", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.36.0.tgz", - "integrity": "sha512-U0GQd5yFvV5qUtT41X1zCQfbw14vkbbKwLlQXhdylEmgpYVHkefXYcC4HHwWOfZa3x6Y8wxDLUBv7dxN5XQ3nA==", + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.39.1.tgz", + "integrity": "sha512-GhLxH1xwnqa5VjhJ29LfcQITNDp+f1jzmMPXQfGW9oNrF0lfjPzKvV5y/bjIQkyKpwCX3Fp+GA4dBpMMk8g+ZQ==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.20.0", diff --git a/package.json b/package.json index 9e63960157..15d9d4707a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.6.1", + "version": "0.6.2", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", @@ -66,13 +66,14 @@ "@tiptap/extension-highlight": "^2.10.0", "@tiptap/extension-placeholder": "^2.10.0", "@tiptap/extension-typography": "^2.10.0", - "@tiptap/pm": "^2.10.0", + "@tiptap/pm": "^2.11.7", "@tiptap/starter-kit": "^2.10.0", "@xyflow/svelte": "^0.1.19", "async": "^3.2.5", "bits-ui": "^0.19.7", "codemirror": "^6.0.1", "codemirror-lang-hcl": "^0.0.0-beta.2", + "codemirror-lang-elixir": "^4.0.0", "crc-32": "^1.2.2", "dayjs": "^1.11.10", "dompurify": "^3.1.6", diff --git a/pyproject.toml b/pyproject.toml index 4c420af797..52260e45e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ dependencies = [ "transformers", "sentence-transformers==3.3.1", + "accelerate", "colbert-ai==0.2.21", "einops==0.8.1", diff --git a/src/app.css b/src/app.css index 4061d3b5eb..86e8438f09 100644 --- a/src/app.css +++ b/src/app.css @@ -46,6 +46,14 @@ math { @apply rounded-lg; } +input::placeholder { + direction: auto; +} + +textarea::placeholder { + direction: auto; +} + .input-prose { @apply prose dark:prose-invert prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line; } diff --git a/src/lib/components/channel/MessageInput.svelte b/src/lib/components/channel/MessageInput.svelte index 9ee433e30c..9f495a8de1 100644 --- a/src/lib/components/channel/MessageInput.svelte +++ b/src/lib/components/channel/MessageInput.svelte @@ -381,7 +381,7 @@ >
{#if files.length > 0}
diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 2ee588985c..7275baed9a 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -1744,6 +1744,11 @@ history.currentId = userMessageId; await tick(); + + if (autoScroll) { + scrollToBottom(); + } + await sendPrompt(history, userPrompt, userMessageId); }; @@ -1754,6 +1759,10 @@ let userMessage = history.messages[message.parentId]; let userPrompt = userMessage.content; + if (autoScroll) { + scrollToBottom(); + } + if ((userMessage?.models ?? [...selectedModels]).length == 1) { // If user message has only one model selected, sendPrompt automatically selects it for regeneration await sendPrompt(history, userPrompt, userMessage.id); diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 985311a986..0f42985a57 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -510,7 +510,7 @@ >
{#if files.length > 0}
@@ -821,6 +821,7 @@ {:else}