Skip to content

Commit

Permalink
Add event-based API using generate_events, generate_events_async
Browse files Browse the repository at this point in the history
…and a new message type "event".
  • Loading branch information
drazvan committed Jul 18, 2023
1 parent b769bc3 commit 637c491
Show file tree
Hide file tree
Showing 5 changed files with 436 additions and 0 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased


### Added

- [Event-based API](./docs/user_guide/advanced/event-based-api.md) for guardrails.
- Support for message with type "event" in [`LLMRails.generate_async`](./docs/api/nemoguardrails.rails.llm.llmrails.md#method-llmrailsgenerate_async).

### Fixed

- [#58](https://github.com/NVIDIA/NeMo-Guardrails/issues/58): Fix install on Mac OS 13.
Expand Down
251 changes: 251 additions & 0 deletions docs/user_guide/advanced/event-based-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
# Event-based API

You can use a guardrails configuration through an event-based API using [`LLMRails.generate_events_async](../../api/nemoguardrails.rails.llm.llmrails.md#method-llmrailsgenerate_events_async) and [`LLMRails.generate_events](../../api/nemoguardrails.rails.llm.llmrails.md#method-llmrailsgenerate_events).

Example usage:

```python
import json
from nemoguardrails import LLMRails, RailsConfig

config = RailsConfig.from_path("path/to/config")
app = LLMRails(config)

new_events = app.generate_events(events=[{
"type": "user_said",
"content": "Hello! What can you do for me?"
}])
print(json.dumps(new_events, indent=True))
```

Example output:
```yaml
[
{
"type": "start_action",
"action_name": "generate_user_intent",
"action_params": {},
"action_result_key": null,
"is_system_action": true
},
{
"type": "action_finished",
"action_name": "generate_user_intent",
"action_params": {},
"action_result_key": null,
"status": "success",
"return_value": null,
"events": [
{
"type": "user_intent",
"intent": "express greeting"
}
],
"is_system_action": true
},
{
"type": "user_intent",
"intent": "express greeting"
},
{
"type": "bot_intent",
"intent": "express greeting"
},
{
"type": "start_action",
"action_name": "retrieve_relevant_chunks",
"action_params": {},
"action_result_key": null,
"is_system_action": true
},
{
"type": "context_update",
"data": {
"relevant_chunks": ""
}
},
{
"type": "action_finished",
"action_name": "retrieve_relevant_chunks",
"action_params": {},
"action_result_key": null,
"status": "success",
"return_value": "",
"events": null,
"is_system_action": true
},
{
"type": "start_action",
"action_name": "generate_bot_message",
"action_params": {},
"action_result_key": null,
"is_system_action": true
},
{
"type": "context_update",
"data": {
"_last_bot_prompt": "<<REMOVED FOR READABILITY>>>"
}
},
{
"type": "action_finished",
"action_name": "generate_bot_message",
"action_params": {},
"action_result_key": null,
"status": "success",
"return_value": null,
"events": [
{
"type": "bot_said",
"content": "Hello!"
}
],
"is_system_action": true
},
{
"type": "bot_said",
"content": "Hello!"
},
{
"type": "listen"
}
]
```

## Event Types

NeMo Guardrails supports multiple types of events. Some are meant for internal use (e.g., `user_intent`, `bot_intent`), while others represent the "public" interface (e.g., `user_said`, `bot_said`).

### `user_said`

The raw message from the user.

Example:
```json
{
"type": "user_said",
"content": "Hello!"
}
```

### `user_intent`

The computed intent (a.k.a. canonical form) for what the user said.

Example:
```json
{
"type": "user_intent",
"intent": "express greeting"
}
```

### `bot_intent`

The computed intent for what the bot should say.

Example:
```json
{
"type": "bot_intent",
"intent": "express greeting"
}
```

### `bot_said`

The final message from the bot.

Example:
```json
{
"type": "bot_said",
"content": "Hello!"
}
```

### `start_action`

An action needs to be started.

Example:
```json
{
"type": "start_action",
"action_name": "generate_user_intent",
"action_params": {},
"action_result_key": null,
"is_system_action": true
}
```

### `action_finished`

An action has finished.

Example:
```json
{
"type": "action_finished",
"action_name": "generate_user_intent",
"action_params": {},
"action_result_key": null,
"status": "success",
"return_value": null,
"events": [
{
"type": "user_intent",
"intent": "express greeting"
}
],
"is_system_action": true
}
```

### `context_update`

The context of the conversation has been updated.

Example:
```json
{
"type": "context_update",
"data": {
"user_name": "John"
}
}
```

### `listen`

The bot has finished processing the events and is waiting for new input.

Example:
```json
{
"type": "listen"
}
```

## Custom Events

You can also use custom events:

```json
{
"type": "some_other_type",
...
}
```

**Note**: You need to make sure that the guardrails logic can handle the custom event.

## Typical Usage

Typically, you will need to:

1. Persist the events history for a particular user in a database.
2. Whenever there is a new message or another event, you fetch the history and append the new event.
3. Use the guardrails API to generate the next events.
4. Filter the `bot_said` events and return them to the user.
5. Persist the history of events back into the database.
55 changes: 55 additions & 0 deletions nemoguardrails/rails/llm/llmrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ def _get_events_for_messages(self, messages: List[dict]):
events.append({"type": "bot_said", "content": msg["content"]})
elif msg["role"] == "context":
events.append({"type": "context_update", "data": msg["content"]})
elif msg["role"] == "event":
events.append(msg["event"])

return events

Expand All @@ -194,6 +196,7 @@ async def generate_async(
{"role": "context", "content": {"user_name": "John"}},
{"role": "user", "content": "Hello! How are you?"},
{"role": "assistant", "content": "I am fine, thank you!"},
{"role": "event", "event": {"type": "user_silent"}},
...
]
```
Expand Down Expand Up @@ -273,6 +276,58 @@ def generate(

return asyncio.run(self.generate_async(prompt=prompt, messages=messages))

async def generate_events_async(self, events: List[dict]) -> List[dict]:
"""Generate the next events based on the provided history.
The format for events is the following:
```python
[
{"type": "...", ...},
...
]
```
Args:
events: The history of events to be used to generate the next events.
Returns:
The newly generate event(s).
"""
t0 = time.time()
llm_stats.reset()

# Compute the new events.
new_events = await self.runtime.generate_events(events)

# If logging is enabled, we log the conversation
# TODO: add support for logging flag
if self.verbose:
history = get_colang_history(events)
log.info(f"Conversation history so far: \n{history}")

log.info("--- :: Total processing took %.2f seconds." % (time.time() - t0))
log.info("--- :: Stats: %s" % llm_stats)

return new_events

def generate_events(self, events: List[dict]) -> List[dict]:
"""Synchronous version of `LLMRails.generate_events_async`."""

try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None

if loop and loop.is_running():
raise RuntimeError(
"You are using the sync `generate_events` inside async code. "
"You should replace with `await generate_events_async(...)."
)

return asyncio.run(self.generate_events_async(events=events))

def register_action(self, action: callable, name: Optional[str] = None):
"""Register a custom action for the rails configuration."""
self.runtime.register_action(action, name)
Expand Down
2 changes: 2 additions & 0 deletions nemoguardrails/rails/llm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ def get_history_cache_key(messages: List[dict]) -> str:
key_items.append(msg["content"])
elif msg["role"] == "context":
key_items.append(json.dumps(msg["content"]))
elif msg["role"] == "event":
key_items.append(json.dumps(msg["event"]))

history_cache_key = ":".join(key_items)

Expand Down
Loading

0 comments on commit 637c491

Please sign in to comment.