Packages

Hexagonal architecture core for building flexible RAG systems in Elixir.

Current section

Files

Jump to
portfolio_core lib portfolio_core.ex
Raw

lib/portfolio_core.ex

defmodule PortfolioCore do
@moduledoc """
Hexagonal architecture core for building flexible RAG systems in Elixir.
PortfolioCore provides the foundational primitives for building
Retrieval-Augmented Generation (RAG) systems using a ports and adapters
(hexagonal) architecture.
## What This Package Provides
- **Port Specifications** - Elixir behaviors defining contracts for adapters
- **Manifest Engine** - YAML-based configuration for adapter wiring
- **Adapter Registry** - Dynamic lookup of registered adapters
- **Telemetry Integration** - Observability hooks for monitoring
## What This Package Does NOT Provide
- No concrete adapter implementations (use `portfolio_index` for those)
- No database schemas or migrations
- No LLM/embedding API calls
- No Broadway pipelines
## Architecture Overview
┌─────────────────────────────────────────┐
│ YOUR APPLICATION │
│ (portfolio_manager) │
└─────────────────┬───────────────────────┘
┌─────────────────▼───────────────────────┐
│ PORTFOLIO_CORE │
│ ┌───────────────────────────────────┐ │
│ │ PORTS │ │
│ │ VectorStore, GraphStore, │ │
│ │ Embedder, LLM, Chunker, │ │
│ │ Retriever, Reranker, DocumentStore│ │
│ └───────────────────────────────────┘ │
│ ┌───────────────────────────────────┐ │
│ │ MANIFEST ENGINE │ │
│ │ YAML loading, validation, │ │
│ │ adapter wiring │ │
│ └───────────────────────────────────┘ │
│ ┌───────────────────────────────────┐ │
│ │ REGISTRY │ │
│ │ ETS-based adapter lookup │ │
│ └───────────────────────────────────┘ │
└─────────────────┬───────────────────────┘
┌─────────────────▼───────────────────────┐
│ PORTFOLIO_INDEX │
│ Concrete adapter implementations: │
│ Pgvector, Neo4j, OpenAI, Gemini, etc. │
└─────────────────────────────────────────┘
## Quick Start
1. Add `portfolio_core` to your dependencies:
```elixir
defp deps do
[{:portfolio_core, "~> 0.1.0"}]
end
```
2. Create a manifest file (`config/manifest.yaml`):
```yaml
version: "1.0"
environment: dev
adapters:
vector_store:
adapter: MyApp.Adapters.Pgvector
config:
repo: MyApp.Repo
embedder:
adapter: MyApp.Adapters.OpenAI
config:
model: text-embedding-3-small
api_key: ${OPENAI_API_KEY}
```
3. Configure the manifest path:
```elixir
config :portfolio_core, :manifest,
manifest_path: "config/manifest.yaml"
```
4. Use adapters in your code:
```elixir
{adapter, config} = PortfolioCore.Registry.get!(:vector_store)
adapter.search(config, index_id, query_vector, 10)
```
## Ports
Ports are Elixir behaviors that define contracts for adapters:
- `PortfolioCore.Ports.VectorStore` - Vector storage and similarity search
- `PortfolioCore.Ports.GraphStore` - Graph database operations
- `PortfolioCore.Ports.DocumentStore` - Document storage
- `PortfolioCore.Ports.Embedder` - Embedding generation
- `PortfolioCore.Ports.LLM` - Language model completions
- `PortfolioCore.Ports.Chunker` - Document chunking
- `PortfolioCore.Ports.Retriever` - Retrieval strategies
- `PortfolioCore.Ports.Reranker` - Result reranking
"""
alias PortfolioCore.Manifest.Engine
@doc """
Get the currently loaded manifest.
## Returns
- The manifest as a keyword list
- `nil` if no manifest is loaded
## Example
manifest = PortfolioCore.manifest()
version = Keyword.get(manifest, :version)
"""
@spec manifest() :: keyword() | nil
def manifest do
Engine.get_manifest()
end
@doc """
Get an adapter for the given port.
## Parameters
- `port_name` - Atom identifying the port (e.g., `:vector_store`)
## Returns
- `{module, config}` tuple for the adapter
- `nil` if no adapter is registered
## Example
{adapter, config} = PortfolioCore.adapter(:vector_store)
adapter.search(config[:index], query_vector, 10)
"""
@spec adapter(atom()) :: {module(), keyword()} | nil
def adapter(port_name) do
PortfolioCore.Registry.get(port_name)
end
@doc """
Get an adapter for the given port, raising if not found.
## Parameters
- `port_name` - Atom identifying the port
## Returns
- `{module, config}` tuple for the adapter
## Raises
- `ArgumentError` if no adapter is registered for the port
## Example
{adapter, config} = PortfolioCore.adapter!(:embedder)
"""
@spec adapter!(atom()) :: {module(), keyword()}
def adapter!(port_name) do
PortfolioCore.Registry.get!(port_name)
end
@doc """
List all registered port names.
## Returns
- List of atoms representing registered ports
## Example
PortfolioCore.registered_ports()
# => [:vector_store, :embedder, :chunker]
"""
@spec registered_ports() :: [atom()]
def registered_ports do
PortfolioCore.Registry.list_ports()
end
@doc """
Reload the manifest from disk.
## Returns
- `:ok` on success
- `{:error, reason}` on failure
## Example
:ok = PortfolioCore.reload_manifest()
"""
@spec reload_manifest() :: :ok | {:error, term()}
def reload_manifest do
Engine.reload()
end
end