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