diff --git a/README.md b/README.md index 14b50e88..08036f8d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@

- Chat · Search your files · Run scripts · Build multi-step AI workflows · Execute AI agent skills - all offline-capable, all on your machine. + Chat · Search your files and the web · Run scripts · Build multi-step AI workflows · Execute AI agent skills - all offline-capable, all on your machine.

@@ -129,11 +129,12 @@ You shouldn't have to choose between the best AI model, your privacy, and gettin ## Features - **Multi-provider** - Switch between OpenAI, Claude, Gemini, Grok, Ollama, LM Studio, LocalAI, Docker AI, or any OpenAI-compatible endpoint per session +- **Web search (multiple backends)** - Search the web with DuckDuckGo (no API key), Brave Search API, Tavily, or your own SearxNG instance - **Local RAG** - Index local folders, files, and web URLs. Hybrid BM25 + vector retrieval with an AI classifier that skips retrieval when the query doesn't need it. Your data never leaves your machine. - **Plans (agentic workflows)** - Chain multi-step AI pipelines from a form UI. Each step builds on the previous; progress shown live. Export as PDF or Word. Define your own plans in YAML or generate them by describing your workflow in plain English. - **Skills** - Define reusable AI agents as Markdown files and execute them via [Gemini CLI](https://github.com/google-gemini/gemini-cli) or [Claude Code](https://docs.anthropic.com/en/docs/claude-code). Skills carry a system prompt, run in a sandboxed workspace, stream live activity, and persist a full run history. - **Script runner** - Execute Python, Bash, and JavaScript from chat. Python runs in an auto-managed virtualenv with automatic dependency installation. -- **MCP tool integration** - Connect MCP-compatible servers via stdio or HTTP, scoped globally or per project +- **MCP tool integration** - Connect MCP-compatible servers via stdio or HTTP - **Persistent sessions** - Conversations stored in a local SQLite database, restored on restart - **Vision** - Attach images to conversations; works with any multimodal model - **Local telemetry** - Token usage, cost estimates, RAG performance per provider. Nothing uploaded. @@ -234,4 +235,3 @@ Join the [Askimo Discord](https://discord.gg/eXSBR4fNmm) for help, feedback, and - diff --git a/shared/src/main/kotlin/io/askimo/core/util/HttpUtils.kt b/shared/src/main/kotlin/io/askimo/core/util/HttpUtils.kt index 77dd6dda..07c4fa9f 100644 --- a/shared/src/main/kotlin/io/askimo/core/util/HttpUtils.kt +++ b/shared/src/main/kotlin/io/askimo/core/util/HttpUtils.kt @@ -4,11 +4,13 @@ */ package io.askimo.core.util +import java.io.ByteArrayInputStream import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse import java.time.Duration +import java.util.zip.GZIPInputStream /** * Perform a GET request to a local/remote URL with proxy support. @@ -29,8 +31,9 @@ fun httpGet( .timeout(Duration.ofMillis(readTimeoutMs)) .GET() headers.forEach { (k, v) -> requestBuilder.header(k, v) } - val response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()) - return response.statusCode() to response.body() + val response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray()) + val decompressedBody = decompressGzipIfNeeded(response.body(), response) + return response.statusCode() to decompressedBody } /** @@ -54,6 +57,22 @@ fun httpPost( .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) headers.forEach { (k, v) -> requestBuilder.header(k, v) } - val response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()) - return response.statusCode() to response.body() + val response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray()) + val decompressedBody = decompressGzipIfNeeded(response.body(), response) + return response.statusCode() to decompressedBody +} + +/** + * Decompresses gzip-encoded response body if the Content-Encoding header indicates gzip. + * Otherwise returns the body as-is. + */ +private fun decompressGzipIfNeeded(body: ByteArray, response: HttpResponse): String { + val encoding = response.headers() + .allValues("Content-Encoding") + .firstOrNull { it.contains("gzip", ignoreCase = true) } ?: "" + return if (encoding.isNotEmpty()) { + GZIPInputStream(ByteArrayInputStream(body)).bufferedReader().use { it.readText() } + } else { + String(body, Charsets.UTF_8) + } }