# `core`: Developer Tools for LLMs

The `core` module provides foundational functionality used by other modules.
We recommend reading the other quickstart sections before this one.

## Metadata

The `Metadata` class provides a type-safe, dictionary-like interface for managing metadata in sessions and messages. It supports all common dictionary operations while ensuring type safety and proper data handling.

```python
from prompttrail.core import Metadata, Session, Message

# Creating metadata
metadata = Metadata()
metadata["key"] = "value"
metadata.update({"another_key": 42})

# Using with Session
session = Session(metadata={"user_id": "123"})
session.metadata["language"] = "ja"

# Using with Message
message = Message(
    content="Hello",
    role="user",
    metadata={"timestamp": "2024-01-26"}
)
```

Key features of `Metadata`:
- Dictionary-like operations (get, set, update)
- Support for complex value types (strings, numbers, lists, dictionaries)
- Copy operations that maintain independence
- Type safety and proper data handling


(cache)=
## Cache

LLM APIs are sometimes expensive. PromptTrail has a built-in cache mechanism to reduce the number of API calls.

Cache is implemented as a `CacheProvider`. You can pass a `CacheProvider` to the model's configuration.

Let's see an example:

```python
from prompttrail.core.cache import LRUCacheProvider
from prompttrail.models.openai import OpenAIConfig, OpenAIModel

config = OpenAIConfig(
    api_key=api_key,
    model_name="gpt-4o-mini",
    max_tokens=100,
    # Just pass a cache provider to the configuration
    cache_provider=LRUCacheProvider()
)
model = OpenAIModel(config)
```

We passed a `LRUCacheProvider` to the configuration. And the configuration is passed to the model.
Now, the model will cache the messages.
Just call `send` method as usual. If you pass the same `session`, you will get the cached message.

```python
from prompttrail.core import Session, Message

session = Session(
    messages = [Message(content="Hello, I'm a human.", role="user")]
)
# This time, the model calls the API
message_1 = model.send(session=session)
# This time, the model returns the cached message
message_2 = model.send(session=session)
```

Internally, `CacheProvider` has two methods:
- `add(session: Session, message: Message)`: add a new session and message pair to the cache
- `search(session: Session) -> Message`: get a message from the cache

Therefore, `CacheProvider` is basically a simple key-value store.

You don't need to implement `CacheProvider` by yourself. PromptTrail has a built-in `LRUCacheProvider` which is a simple LRU cache. If you want a custom implementation, you can inherit from `CacheProvider` and implement the methods.

(mock)=
## Mock

Say, you successfully build something using LLM. But, how can you test it?
One way is to call the LLM API, of course. But, it may be costly, slow, and even non-deterministic (e.g., [GPT-3.5 and 4 is non-deterministic even if `temperature` is set to `0`](https://152334h.github.io/blog/non-determinism-in-gpt-4/)).


You may want to mock the LLM API. PromptTrail has a built-in mock mechanism.

Mock is implemented as a `MockProvider`. You can pass a `MockProvider` to the model's configuration like `CacheProvider`.

Let's see an example of `OneTurnConversationMockProvider`, which returns a message based on the last message of the session:

```python
from prompttrail.core import Message
from prompttrail.core.mock import OneTurnConversationMockProvider
from prompttrail.models.openai import OpenAIConfig, OpenAIModel

# First, you need to define a table to define how the mock return response based on last message
role = "assistant"
conversation_table = {
    "Hello": Message(content="Hi", role=role),
}
# Then, you can pass the table to the mock provider and pass the mock provider to the configuration
config = OpenAIConfig(
    api_key="dummy",  # API key is not used when using mock provider
    model_name="gpt-4o-mini",
    max_tokens=100,
    mock_provider=OneTurnConversationMockProvider(conversation_table)
)
model = OpenAIModel(config)
```

Let's call the model:
```python
from prompttrail.core import Session, Message

session = Session(
    messages = [Message(content="Hello", role="user")]
)
message = model.send(session=session)
assert message.content == "Hi"
```

As the session's last message is "Hello", the mock provider returns "Hi" based on the table.

There're other mock providers:

- `EchoMockProvider`: returns the same message as the last message of the session
- `FunctionMockProvider`: returns a message based on a function you defined

The usage of `MockProvider` is very similar to `CacheProvider`. You can pass a `MockProvider` to the model's configuration like `CacheProvider`.

Actually, the role of `MockProvider` is very similar to that of `CacheProvider`. Because both return a `Message` object based on `Session` object. There're two differences:

- `CacheProvider` checks the configuration because LLM may return different messages for the same session with different parameters.
- `CacheProvider` can return `None` if the message is not in the cache. `MockProvider` always returns a `Message` object.


```{Note}
`MockProvider` and `CacheProvider` cannot be used at the same time.
```

## Debug and Logging

PromptTrail provides comprehensive debugging and logging capabilities to help you understand and troubleshoot your LLM applications.

### Debug Mode

Debug mode can be enabled at both the runner and session level:

```python
# Enable debug mode when running
session = runner.run(debug_mode=True)

# Or when creating a session
session = Session(debug_mode=True)
```

When debug mode is enabled, you'll see detailed information about:
- Template rendering process
- Message generation and transformation
- Tool execution and results
- Cache hits and misses
- API calls and responses

### Logging

PromptTrail uses Python's standard logging module. You can configure logging to see different levels of detail:

```python
import logging

# Set logging level for all PromptTrail components
logging.basicConfig(level=logging.DEBUG)

# Or configure specific loggers
logger = logging.getLogger("prompttrail")
logger.setLevel(logging.INFO)

# Add handlers for custom output
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logger.addHandler(handler)
```

Common logging areas include:
- Model interactions (`prompttrail.models`)
- Runner execution (`prompttrail.agent.runners`)
- Template processing (`prompttrail.agent.templates`)
- Tool execution (`prompttrail.agent.tools`)
- Cache operations (`prompttrail.core.cache`)

### Debuggable Base Class

All major components in PromptTrail inherit from the `Debuggable` base class, which provides consistent debugging capabilities:

```python
from prompttrail.core.utils import Debuggable

class MyComponent(Debuggable):
    def my_method(self):
        self.debug("Debug message")
        self.info("Info message")
        self.warning("Warning message")
        self.error("Error message")
```

This ensures consistent logging behavior across the library and makes it easier to add debugging to your own components.
