Skip to content
Open
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
127 changes: 127 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Contributing to lil-agents

Thanks for interest in contributing! Here's how to get started.

## Local LLM Support

This project supports multiple local LLM backends for privacy-first AI interactions.

### Supported Local LLMs

#### Ollama (Recommended for beginners)

Easiest to use with pre-quantized models:

```bash
brew install ollama
ollama serve
```

In another terminal:
```bash
ollama run llama2
```

#### vLLM (High-performance inference)

For faster inference with HuggingFace models:

```bash
python3 -m venv ~/.venv
source ~/.venv/bin/activate
pip install vllm
python -m vllm.entrypoints.openai.api_server
```

#### Llama.cpp (Lightweight)

Minimal resource footprint, great for older machines:

```bash
brew install llama.cpp
llama-cli -m <path-to-model.gguf> -i
```

### Adding a New Local LLM Provider

1. Add a new case to `AgentProvider` enum in `LilAgents/AgentSession.swift`:
```swift
case localMyLLM
```

2. Update provider properties:
```swift
var displayName: String {
switch self {
case .localMyLLM: return "Local • MyLLM"
// ...
}
}

var binaryName: String {
switch self {
case .localMyLLM: return "myllm"
// ...
}
}
```

3. Add installation instructions:
```swift
var installInstructions: String {
switch self {
case .localMyLLM:
return "Installation instructions for MyLLM..."
// ...
}
}
```

4. Update factory method:
```swift
func createSession() -> any AgentSession {
switch self {
case .localMyLLM: return LocalLLMSession(provider: .localMyLLM)
// ...
}
}
```

5. Add binary search paths in `detectAvailableProviders()` if needed

6. Test with your LLM installed locally

## Code Style

- Follow Swift naming conventions (camelCase for variables/functions, PascalCase for types)
- Use `private` for internal properties
- Add `// MARK: -` comments to organize code sections
- Document public functions with comments
- Use descriptive variable names
- Prefer `guard let` for optional unwrapping

## Testing

1. Install a local LLM (Ollama recommended)
2. Build the app in Xcode: `open lil-agents.xcodeproj`
3. Run and select the LLM from Provider menu
4. Test interactive chat

## Building

```bash
open lil-agents.xcodeproj
```

Build with Xcode or via command line:
```bash
xcodebuild -scheme LilAgents -configuration Release
```

## Submitting Changes

1. Fork the repo
2. Create a feature branch: `git checkout -b feature/my-feature`
3. Commit with clear messages
4. Push and open a PR to `ryanstephen/lil-agents`
5. Reference any related issues
129 changes: 109 additions & 20 deletions LilAgents/AgentSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Foundation

enum AgentProvider: String, CaseIterable {
case claude, codex, copilot, gemini, opencode, openclaw
case localLlama, localOllama, localVllm

private static let defaultsKey = "selectedProvider"

Expand All @@ -19,12 +20,15 @@ enum AgentProvider: String, CaseIterable {

var displayName: String {
switch self {
case .claude: return "Claude"
case .codex: return "Codex"
case .copilot: return "Copilot"
case .gemini: return "Gemini"
case .opencode: return "OpenCode"
case .openclaw: return "OpenClaw"
case .claude: return "Claude"
case .codex: return "Codex"
case .copilot: return "Copilot"
case .gemini: return "Gemini"
case .opencode: return "OpenCode"
case .openclaw: return "OpenClaw"
case .localLlama: return "Local • Llama"
case .localOllama: return "Local • Ollama"
case .localVllm: return "Local • vLLM"
}
}

Expand All @@ -43,12 +47,15 @@ enum AgentProvider: String, CaseIterable {

var binaryName: String {
switch self {
case .claude: return "claude"
case .codex: return "codex"
case .copilot: return "copilot"
case .gemini: return "gemini"
case .opencode: return "opencode"
case .openclaw: return "openclaw"
case .claude: return "claude"
case .codex: return "codex"
case .copilot: return "copilot"
case .gemini: return "gemini"
case .opencode: return "opencode"
case .openclaw: return "openclaw"
case .localLlama: return "llama"
case .localOllama: return "ollama"
case .localVllm: return "vllm"
}
}

Expand All @@ -59,23 +66,40 @@ enum AgentProvider: String, CaseIterable {
static func detectAvailableProviders(completion: @escaping () -> Void) {
let all = AgentProvider.allCases
let group = DispatchGroup()

for provider in all {
// OpenClaw is network-based, not a local binary
if provider == .openclaw {
availability[provider] = OpenClawConfig.load().authToken.isEmpty == false
continue
}

group.enter()
let home = FileManager.default.homeDirectoryForCurrentUser.path
ShellEnvironment.findBinary(name: provider.binaryName, fallbackPaths: [
var fallbackPaths = [
"\(home)/.local/bin/\(provider.binaryName)",
"/usr/local/bin/\(provider.binaryName)",
"/opt/homebrew/bin/\(provider.binaryName)"
]) { path in
]

// Add extra paths for local LLMs
switch provider {
case .localOllama:
fallbackPaths.append("/opt/ollama/bin/ollama")
fallbackPaths.append("\(home)/.ollama/bin/ollama")
case .localVllm:
fallbackPaths.append("\(home)/.venv/bin/vllm")
fallbackPaths.append("/usr/local/bin/vllm")
default:
break
}

ShellEnvironment.findBinary(name: provider.binaryName, fallbackPaths: fallbackPaths) { path in
availability[provider] = path != nil
group.leave()
}
}

group.notify(queue: .main) {
completion()
}
Expand Down Expand Up @@ -105,17 +129,82 @@ enum AgentProvider: String, CaseIterable {
return "To install, run this in Terminal:\n curl -fsSL https://opencode.ai/install | bash"
case .openclaw:
return "OpenClaw is a self-hosted AI gateway.\n\nInstall: npm install -g openclaw\nStart: openclaw gateway run\n\nDocs: https://docs.openclaw.ai"
case .localLlama:
return "To install Llama locally:\n\n1. Download from: https://github.com/ggerganov/llama.cpp\n2. Build: make\n3. Place binary in /usr/local/bin or ~/.local/bin\n\nOr use Homebrew:\n brew install llama.cpp"
case .localOllama:
return "To install Ollama:\n\n1. Download from: https://ollama.ai\n2. Install the app\n3. Run: ollama serve (in background)\n4. Test: ollama run llama2\n\nOr with Homebrew:\n brew install ollama"
case .localVllm:
return "To install vLLM:\n\n1. Create virtual env: python3 -m venv ~/.venv\n2. Activate: source ~/.venv/bin/activate\n3. Install: pip install vllm\n4. Start server: python -m vllm.entrypoints.openai.api_server"
}
}

func createSession() -> any AgentSession {
switch self {
case .claude: return ClaudeSession()
case .codex: return CodexSession()
case .copilot: return CopilotSession()
case .gemini: return GeminiSession()
case .opencode: return OpenCodeSession()
case .openclaw: return OpenClawSession()
case .claude: return ClaudeSession()
case .codex: return CodexSession()
case .copilot: return CopilotSession()
case .gemini: return GeminiSession()
case .opencode: return OpenCodeSession()
case .openclaw: return OpenClawSession()
case .localLlama: return LocalLLMSession(provider: .localLlama)
case .localOllama: return LocalLLMSession(provider: .localOllama)
case .localVllm: return LocalLLMSession(provider: .localVllm)
}
}
}

// MARK: - Local LLM Configuration

/// Stores and manages per-provider configuration for local LLMs.
struct LocalLLMConfig {
let provider: AgentProvider
var endpoint: String
var modelName: String
var apiKey: String
var temperature: Float
var maxTokens: Int

private static let defaults = UserDefaults.standard

/// Load configuration from UserDefaults, with sensible defaults.
static func load(for provider: AgentProvider) -> LocalLLMConfig {
let prefix = provider.rawValue
return LocalLLMConfig(
provider: provider,
endpoint: defaults.string(forKey: "\(prefix)_endpoint") ?? defaultEndpoint(for: provider),
modelName: defaults.string(forKey: "\(prefix)_model") ?? defaultModel(for: provider),
apiKey: defaults.string(forKey: "\(prefix)_apikey") ?? "",
temperature: Float(defaults.double(forKey: "\(prefix)_temperature")) == 0 ? 0.7 : Float(defaults.double(forKey: "\(prefix)_temperature")),
maxTokens: defaults.integer(forKey: "\(prefix)_maxTokens") == 0 ? 2048 : defaults.integer(forKey: "\(prefix)_maxTokens")
)
}

/// Persist configuration to UserDefaults.
mutating func save() {
let prefix = provider.rawValue
let defaults = UserDefaults.standard
defaults.set(endpoint, forKey: "\(prefix)_endpoint")
defaults.set(modelName, forKey: "\(prefix)_model")
defaults.set(apiKey, forKey: "\(prefix)_apikey")
defaults.set(Double(temperature), forKey: "\(prefix)_temperature")
defaults.set(maxTokens, forKey: "\(prefix)_maxTokens")
}

private static func defaultEndpoint(for provider: AgentProvider) -> String {
switch provider {
case .localOllama: return "http://localhost:11434"
case .localVllm: return "http://localhost:8000"
case .localLlama: return "http://localhost:8080"
default: return "http://localhost:8000"
}
}

private static func defaultModel(for provider: AgentProvider) -> String {
switch provider {
case .localOllama: return "llama2"
case .localVllm: return "meta-llama/Llama-2-7b-hf"
case .localLlama: return "default"
default: return "default"
}
}
}
Expand Down
Loading