add more architecture docs
This commit is contained in:
53
architecture/agents.md
Normal file
53
architecture/agents.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Agents
|
||||||
|
|
||||||
|
Agents can be viewed as an FSM using an LLM to generate inputs into the system that operates over a DAG.
|
||||||
|
|
||||||
|
What this really means is that the agent is just a function without memory that uses text inputs and outputs in a
|
||||||
|
defined order.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def my_agent(*args, **kwargs) -> str:
|
||||||
|
# do whatever you want!
|
||||||
|
return "Hi I'm an agent!"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now obviously, that's like saying water's wet, but we're going to be using that definition to inform our design of the
|
||||||
|
library, namely, that we should *not* store agent state outside the function call.
|
||||||
|
|
||||||
|
## The Agent Class
|
||||||
|
|
||||||
|
So we don't have state, why are we using a class?
|
||||||
|
|
||||||
|
Well, we want to initialize things, we want to have some configuration, and we want to have some helper functions.
|
||||||
|
Preferably all in a single place.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BaseAgent:
|
||||||
|
def agent_primitives(self) -> list[BaseAgent]:
|
||||||
|
# Returns a list of Agents that are utilized by this agent to generate inputs
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def tools(self) -> list[BaseTool]:
|
||||||
|
# Returns a list of tools that the agent needs to run
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def run(self, config, *args, **kwargs) -> ConversationGraph:
|
||||||
|
llm = get_llm(config)
|
||||||
|
tools = self.tools()
|
||||||
|
for agent in self.agent_primitives():
|
||||||
|
tools.extend(agent.tools())
|
||||||
|
tools = set(tools)
|
||||||
|
tools = initialize_tools(tools, config)
|
||||||
|
return self(llm, tools, config, *args, **kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __call__(self, llm, tools, config, *args, **kwargs) -> ConversationGraph:
|
||||||
|
# Returns a ConversationGraph that can be parsed to get the output of the agent
|
||||||
|
# Use w/e args/kwargs you want, as long as llm/tools/config are satisfied.
|
||||||
|
raise NotImplementedError
|
||||||
|
```
|
||||||
|
|
||||||
|
Doesn't seem too bad (I hope), it is a bit annoying that we don't initialize everything in the constructor, but
|
||||||
|
hopefully we all kinda like it :)
|
||||||
|
|
||||||
14
architecture/llm_client.md
Normal file
14
architecture/llm_client.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# LLM Client
|
||||||
|
|
||||||
|
A quick wrapper over openai apis
|
||||||
|
|
||||||
|
## Responsibilities
|
||||||
|
|
||||||
|
- Transform "normal" chat/completions requests into graphs
|
||||||
|
- Translate graphs into LLM requests
|
||||||
|
- Keep a history of graphs parsed by it
|
||||||
|
- On Policy Data
|
||||||
|
- Deduplicating graphs, so we don't keep previous history as separate graphs
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
Exactly the same as the openai api! Just with the additional support of graph inputs and outputs.
|
||||||
@@ -45,7 +45,7 @@ graph TD
|
|||||||
class ChatMetadata,ChatResponseText,ChatContent,ToolMetadata,ToolChat,ToolContent,ToolMetadataLength metadataNode
|
class ChatMetadata,ChatResponseText,ChatContent,ToolMetadata,ToolChat,ToolContent,ToolMetadataLength metadataNode
|
||||||
```
|
```
|
||||||
|
|
||||||
Messages should be a graph of immutable elements.
|
Messages should be a graph (DAG, specifically) of immutable elements.
|
||||||
|
|
||||||
## Why immutable elements?
|
## Why immutable elements?
|
||||||
We want to train on policy
|
We want to train on policy
|
||||||
@@ -54,9 +54,60 @@ We want to train on policy
|
|||||||
## Why a graph?
|
## Why a graph?
|
||||||
Nodes and connections are a natural way to represent the flow of information in an agent conversation.
|
Nodes and connections are a natural way to represent the flow of information in an agent conversation.
|
||||||
|
|
||||||
|
|
||||||
## Will this be annoying to deal with?
|
## Will this be annoying to deal with?
|
||||||
|
|
||||||
It shouldn't be! While there will be internal stuff that may look ???, for the interface, it should be as simple as your
|
It shouldn't be! While there will be internal stuff that may look ???, for the interface, it should be as simple as your
|
||||||
normal context window edits, so `message_history[2]['content'] = my_edit`, but internally we'll deal with the recordkeeping
|
normal context window edits, so `message_history[2]['content'] = my_edit`, but internally we'll deal with the recordkeeping
|
||||||
and how this ends up parsing into on policy training data, if requested.
|
and how this ends up parsing into on policy training data, if requested.
|
||||||
|
|
||||||
|
## Edges
|
||||||
|
|
||||||
|
Edges are the connections between nodes, and there are two types we are concerned with:
|
||||||
|
- **Sequential edges**: These represent the flow of conversation, connecting messages in the order they were sent. For example, a user message followed by an assistant response.
|
||||||
|
- **Parallel edges**: These represent versioning, e.g. edit history, context squishing, etc.
|
||||||
|
|
||||||
|
## So what does this look like in practice?
|
||||||
|
|
||||||
|
```python
|
||||||
|
import copy
|
||||||
|
|
||||||
|
|
||||||
|
class MessageGraph:
|
||||||
|
def __init__(self):
|
||||||
|
self.messages = []
|
||||||
|
self.prev_graph = None
|
||||||
|
|
||||||
|
def append(self, message):
|
||||||
|
self.messages.append(message)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return self.messages[index]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
# check if an assistant message is after this indx
|
||||||
|
needs_new_graph = False
|
||||||
|
first_idx = -1
|
||||||
|
for i in range(key, len(self.messages)):
|
||||||
|
if (i == key) and (value['role'] == 'assistant') and (value['content'] == self.messages[i]['content']):
|
||||||
|
# no op
|
||||||
|
return
|
||||||
|
needs_new_graph = needs_new_graph or (self.messages[i]['role'] == 'assistant')
|
||||||
|
if needs_new_graph and first_idx == -1:
|
||||||
|
first_idx = i
|
||||||
|
if needs_new_graph:
|
||||||
|
self.prev_graph = copy.deepcopy(self)
|
||||||
|
self.messages[key] = value
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.messages)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return "\n\n".join(f"{msg['role']}: {msg['content']}" for msg in self) == "\n\n".join(
|
||||||
|
f"{msg['role']}: {msg['content']}" for msg in other)
|
||||||
|
|
||||||
|
|
||||||
|
# in use
|
||||||
|
messages = MessageGraph()
|
||||||
|
messages.append({'role': 'system', 'content': 'Hello, I am a system message'})
|
||||||
|
messages[0] = {'role': 'user', 'content': 'Hello, I am a user message'}
|
||||||
|
```
|
||||||
16
architecture/tools.md
Normal file
16
architecture/tools.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Tools
|
||||||
|
|
||||||
|
Not much on this, yet. Tools are just a stateful wrapper around a function, so we can do things like:
|
||||||
|
- Keep a docker container running
|
||||||
|
- Keep a game online
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BaseTool:
|
||||||
|
def definitions(self) -> List[Dict[str, Any]]:
|
||||||
|
# OpenAI API compatible definitions
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs) -> Dict[str, Any]:
|
||||||
|
# Returns at minimum {'role': 'tool', 'content': '...'}
|
||||||
|
raise NotImplementedError
|
||||||
|
```
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
firecrawl-py
|
firecrawl-py
|
||||||
openai
|
openai
|
||||||
fal-client
|
fal-client
|
||||||
|
fire
|
||||||
|
git@github.com:NousResearch/hecate.git
|
||||||
|
tenacity
|
||||||
Reference in New Issue
Block a user