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
117 changes: 117 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,123 @@ implementation("com.locanara:locanara:1.0.0")

---

## Pipeline DSL

Compose multiple AI steps into a single type-safe workflow. Each step's output becomes the next step's input, and the return type is determined by the last step.

### Basic Pipeline (two steps)

**Swift**

```swift
import Locanara

let model = FoundationLanguageModel()

// Step 1: fix typos
let proofread = try await model.proofread(
"Ths is a tset of on-devce AI."
)

// Step 2: translate the corrected text
let translated = try await model.translate(
proofread.correctedText, to: "ko"
)
print(translated.translatedText)
```

**Kotlin**

```kotlin
import com.locanara.dsl.*
import com.locanara.platform.PromptApiModel

suspend fun example(context: Context) {
val model = PromptApiModel(context)

// Step 1: fix typos
val proofread = model.proofread(
"Ths is a tset of on-devce AI."
)

// Step 2: translate the corrected text
val translated = model.translate(
proofread.correctedText, to = "ko"
)
println(translated.translatedText)
}
```
Comment thread
hyochan marked this conversation as resolved.

### Declarative Pipeline Builder (Swift)

Swift's `@PipelineBuilder` result builder enforces return types at compile time. The compiler rejects pipelines with incompatible step types, making multi-step workflows safe to refactor.

```swift
import Locanara

let model = FoundationLanguageModel()

// Two-step: proofread → translate
// Return type is TranslateResult — compiler enforced
let result = try await model.pipeline {
Proofread()
Translate(to: "ko")
}.run("Ths is a tset sentece about on-devce AI.")

print(result.translatedText) // "이것은 온디바이스 AI에 관한 테스트 문장입니다."
print(result.targetLanguage) // "ko"

// Three-step: summarize → proofread → translate
let threeStep = try await model.pipeline {
Summarize(bulletCount: 3)
Proofread()
Translate(to: "ja")
}.run(longArticle)
// Returns TranslateResult (last step determines the type)
```

### Kotlin Pipeline DSL

```kotlin
import com.locanara.dsl.*
import com.locanara.platform.PromptApiModel

suspend fun pipelineExample(context: Context) {
val model = PromptApiModel(context)

// Fluent pipeline API
val result = model.pipeline()
.proofread()
.translate(to = "ko")
.run("Ths is a tset sentece about on-devce AI.")

// result is TranslateResult (last step determines type)
println(result.translatedText)

// Three-step pipeline
val threeStep = model.pipeline()
.summarize(bulletCount = 3)
.proofread()
.translate(to = "ja")
.run(longArticle)
}
```

### Available Pipeline Steps

| Step | Swift | Kotlin | Output |
| --------- | ------------------------- | -------------------------- | ----------------- |
| Summarize | `Summarize(bulletCount:)` | `.summarize(bulletCount:)` | `SummarizeResult` |
| Classify | `Classify(categories:)` | `.classify(categories:)` | `ClassifyResult` |
| Translate | `Translate(to:)` | `.translate(to:)` | `TranslateResult` |
| Proofread | `Proofread()` | `.proofread()` | `ProofreadResult` |
| Rewrite | `Rewrite(style:)` | `.rewrite(style:)` | `RewriteResult` |
| Extract | `Extract(entityTypes:)` | `.extract(entityTypes:)` | `ExtractResult` |

> **Full tutorial**: [locanara.com/docs/tutorials/pipeline](https://locanara.com/docs/tutorials/pipeline)

---

## Packages

- [**apple**](packages/apple) — iOS/macOS SDK
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ class DocumentChunker(
}

// Move forward with overlap
val moveDistance = maxOf(1, chunkSize - config.chunkOverlap)
val moveDistance = maxOf(1, config.targetChunkSize - config.chunkOverlap)
currentIndex += moveDistance
}

Expand Down
148 changes: 148 additions & 0 deletions packages/android/locanara/src/test/kotlin/com/locanara/ChainsTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package com.locanara

import com.locanara.builtin.ChatChain
import com.locanara.builtin.ClassifyChain
import com.locanara.builtin.ExtractChain
import com.locanara.builtin.ProofreadChain
import com.locanara.builtin.RewriteChain
import com.locanara.builtin.SummarizeChain
import com.locanara.builtin.TranslateChain
import com.locanara.composable.BufferMemory
import com.locanara.core.ChainInput
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test

// MARK: - Built-in Chain Tests

class SummarizeChainTest {
@Test
fun `run returns typed result`() = runBlocking {
val model = MockModel { "This is a summary." }
val chain = SummarizeChain(model = model, bulletCount = 1)

val result = chain.run("Long article text here...")

assertEquals("This is a summary.", result.summary)
assertEquals("Long article text here...".length, result.originalLength)
}

@Test
fun `invoke returns chain output`() = runBlocking {
val model = MockModel { "Summary text" }
val chain = SummarizeChain(model = model)

val output = chain.invoke(ChainInput(text = "input"))

assertEquals("Summary text", output.text)
assertNotNull(output.typed<SummarizeResult>())
}
}

class ClassifyChainTest {
@Test
fun `run returns classify result`() = runBlocking {
val model = MockModel { "positive" }
val chain = ClassifyChain(
model = model,
categories = listOf("positive", "negative")
)

val result = chain.run("Great product!")

assertEquals("positive", result.topClassification.label)
assertEquals(1.0, result.topClassification.score, 0.001)
}
}

class TranslateChainTest {
@Test
fun `run returns translate result`() = runBlocking {
val model = MockModel { "안녕하세요" }
val chain = TranslateChain(model = model, targetLanguage = "ko")

val result = chain.run("Hello")

assertEquals("안녕하세요", result.translatedText)
assertEquals("en", result.sourceLanguage)
assertEquals("ko", result.targetLanguage)
}
}

class RewriteChainTest {
@Test
fun `run returns rewrite result`() = runBlocking {
val model = MockModel { "Good day, how may I assist you?" }
val chain = RewriteChain(model = model, style = RewriteOutputType.PROFESSIONAL)

val result = chain.run("hey whats up")

assertEquals("Good day, how may I assist you?", result.rewrittenText)
assertEquals(RewriteOutputType.PROFESSIONAL, result.style)
}
}

class ProofreadChainTest {
@Test
fun `run returns proofread result`() = runBlocking {
val model = MockModel { "This is a test." }
val chain = ProofreadChain(model = model)

val result = chain.run("Ths is a tset.")

assertEquals("This is a test.", result.correctedText)
assertTrue(result.hasCorrections)
}

@Test
fun `no corrections detected`() = runBlocking {
val model = MockModel { "Already correct." }
val chain = ProofreadChain(model = model)

val result = chain.run("Already correct.")

assertFalse(result.hasCorrections)
}
}

class ChatChainTest {
@Test
fun `run returns chat result`() = runBlocking {
val model = MockModel { "Hi there!" }
val chain = ChatChain(model = model)

val result = chain.run("Hello!")

assertEquals("Hi there!", result.message)
assertTrue(result.canContinue)
}

@Test
fun `chat with memory saves entries`() = runBlocking {
val model = MockModel { "First response" }
val memory = BufferMemory(maxEntries = 10)
val chain = ChatChain(model = model, memory = memory)

chain.run("First message")

val entries = memory.load(ChainInput(text = "test"))
assertEquals(2, entries.size) // user + assistant
}
}

class ExtractChainTest {
@Test
fun `run returns extract result`() = runBlocking {
val model = MockModel { "Tim Cook\nCupertino" }
val chain = ExtractChain(model = model, entityTypes = listOf("person", "location"))

val result = chain.run("Tim Cook lives in Cupertino")

assertEquals(2, result.entities.size)
assertEquals("Tim Cook", result.entities[0].value)
assertEquals("Cupertino", result.entities[1].value)
}
}
Loading
Loading