Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</p>

<p align="center">
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.
</p>

<p align="center">
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -234,4 +235,3 @@ Join the [Askimo Discord](https://discord.gg/eXSBR4fNmm) for help, feedback, and
<a href="https://github.com/askimo-ai/askimo/graphs/contributors">
<img src="https://contrib.rocks/image?repo=askimo-ai/askimo" />
</a>

27 changes: 23 additions & 4 deletions shared/src/main/kotlin/io/askimo/core/util/HttpUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}

/**
Expand All @@ -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<ByteArray>): 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)
}
}
Loading