# Sorcery
Sorcery is a *magical* library for building event-sourced applications in Elixir.
It is inspired by the [Commanded](https://github.com/commanded/commanded) library but aims to provide a simpler, more intuitive API.
## Features
- **Domain-Driven Design**: Define domains as collections of commands and events that encapsulate your business logic
- **Event Sourcing**: All state changes are captured as a sequence of events, providing a complete audit trail
- **Service Layer**: Handle external concerns (emails, databases, APIs) cleanly separated from your domain logic
- **Automatic Process Management**: Domains and Services are automatically managed as GenServers
- **Type Safety**: Leverages Elixir's type system for robust command and event definitions
- **Simple API**: Intuitive command execution and event handling
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `sorcery` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:sorcery, "~> 0.1.0"}
]
end
```
## Architecture
Sorcery is built around two main concepts:
1. **Domains**: Encapsulate your business logic and maintain state through events
2. **Services**: Handle side effects and external interactions
### Domains
Domains are where you define your:
- State fields and their default values
- Events that can occur and how they modify state
- Commands that can be executed and their validation rules
### Services
Services react to domain events and handle:
- External API calls
- Database operations
- Email sending
- Any other side effects
## Basic Usage
Here's an example of building a simple AI chat application:
```elixir
defmodule ChatApp.ConversationDomain do
use Sorcery.Domain,
domain do
# State fields
state :conversation, default: []
state :model, default: "gpt-4o"
state :system_prompt, default: "You are a helpful assistant."
state :token_count, default: 0
# Event definitions
event :user_message_received do
field :message, type: :string
apply fn state, event ->
%{state |
conversation: [%{content: event.message, role: :user} | state.conversation]
}
end
handle fn state, event ->
{:ok, [LLMPromptRequested{system_prompt: state.system_prompt, conversation: state.conversation}]}
end
end
event :llm_prompt_requested do
field :prompt, type: :string
end
event :model_changed do
field :model, type: :string
apply fn state, event ->
%{state | model: event.model}
end
end
event :llm_response_received do
field :response, type: :string
field :token_count, type: :integer
apply fn state, event ->
%{state |
conversation: [%{content: event.response, role: :assistant} | state.conversation],
token_count: state.token_count + event.token_count
}
end
end
command :change_model do
field :model, type: :string
handle fn state, command ->
if command.model in ["gpt-4o", "gpt-4o-mini"] do
{:ok, [ModelChanged{model: command.model}]}
else
{:error, :invalid_model}
end
end
# Command definition
command :send_message do
field :message, type: :string
handle fn state, command ->
{:ok, [UserMessageReceived{message: command.message},LLMPromptRequested{system_prompt: state.system_prompt, conversation: state.conversation}]}
end
end
end
end
end
```
And a corresponding service to handle LLM interactions:
```elixir
defmodule ChatApp.LLMService do
use Sorcery.Service
service do
on ChatApp.ConversationDomain.Events.LLMPromptRequested do
fn event ->
response = OpenAI.chat_completion(
model: event.model,
system: event.system_prompt,
messages: event.conversation
)
[%ChatApp.ConversationDomain.Events.LLMResponseReceived{
response: response,
token_count: response.usage.total_tokens
}]
end
end
end
end
```
### Executing Commands
Commands can be executed easily:
```elixir
Sorcery.execute(ConversationDomain, ConversationDomain.Commands.SendMessage{
message: "Hello, how are you?"
})
```
The domain process will be automatically started if it doesn't exist.
## Best Practices
1. Keep domains focused on business logic
2. Use services for all external interactions
3. Design events to capture meaningful state changes
4. Make commands validate their inputs
5. Keep event handlers pure and predictable
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
This project is licensed under the MIT License - see the LICENSE file for details.
## Documentation
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/sorcery>.