From 066514e2a9be69fff56cd1324c3d90ae031de446 Mon Sep 17 00:00:00 2001 From: Dakota Date: Fri, 12 Sep 2025 17:47:32 -0500 Subject: [PATCH] add more architecture docs --- architecture/agents.md | 53 ++++++++++++++++++++++++++++++++ architecture/llm_client.md | 14 +++++++++ architecture/message_graph.md | 57 +++++++++++++++++++++++++++++++++-- architecture/tools.md | 16 ++++++++++ requirements.txt | 5 ++- 5 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 architecture/agents.md create mode 100644 architecture/llm_client.md create mode 100644 architecture/tools.md diff --git a/architecture/agents.md b/architecture/agents.md new file mode 100644 index 000000000..5f17dd37d --- /dev/null +++ b/architecture/agents.md @@ -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 :) + diff --git a/architecture/llm_client.md b/architecture/llm_client.md new file mode 100644 index 000000000..fe15d23a4 --- /dev/null +++ b/architecture/llm_client.md @@ -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. \ No newline at end of file diff --git a/architecture/message_graph.md b/architecture/message_graph.md index dd65a2872..d81b054d4 100644 --- a/architecture/message_graph.md +++ b/architecture/message_graph.md @@ -45,7 +45,7 @@ graph TD 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? We want to train on policy @@ -54,9 +54,60 @@ We want to train on policy ## Why a graph? Nodes and connections are a natural way to represent the flow of information in an agent conversation. - ## 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 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. \ No newline at end of file +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'} +``` \ No newline at end of file diff --git a/architecture/tools.md b/architecture/tools.md new file mode 100644 index 000000000..b899c5ebd --- /dev/null +++ b/architecture/tools.md @@ -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 +``` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1a12b5845..a8c9eda41 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ firecrawl-py openai -fal-client \ No newline at end of file +fal-client +fire +git@github.com:NousResearch/hecate.git +tenacity \ No newline at end of file