README.md

# 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>.