diff --git a/playground/src/components/editor/code-editor.tsx b/playground/src/components/editor/code-editor.tsx index b29d4a0..828405d 100644 --- a/playground/src/components/editor/code-editor.tsx +++ b/playground/src/components/editor/code-editor.tsx @@ -1,6 +1,6 @@ -import React, { useEffect } from "react"; +import React from "react"; import AceEditor from "react-ace"; -import "ace-builds/src-noconflict/theme-gruvbox_light_hard"; +import "ace-builds/src-noconflict/theme-gruvbox_dark_hard"; import "./simulatrex-mode"; const CodeEditor = ({ @@ -13,7 +13,12 @@ const CodeEditor = ({ return ( = ({ agents }) => { const size = 20; // Example size, this can be dynamic const tileColor = "#EEF0F4"; // Light grey for the tiles const [hoveredAgentId, setHoveredAgentId] = useState(null); + console.log(agents); return ( diff --git a/playground/src/components/resizebale-container.tsx b/playground/src/components/resizebale-container.tsx new file mode 100644 index 0000000..0d3e4e6 --- /dev/null +++ b/playground/src/components/resizebale-container.tsx @@ -0,0 +1,107 @@ +import React, { useRef, useState, useCallback, useEffect } from "react"; + +type ResizableContainerProps = { + children: React.ReactNode; + initialWidth: string; // Using percentages for relative sizing + initialHeight: string; // Using percentages for relative sizing + resizeDirection: "x" | "y" | "both"; + className?: string; +}; + +const ResizableContainer: React.FC = ({ + children, + initialWidth, + initialHeight, + resizeDirection, + className, +}) => { + const [dimensions, setDimensions] = useState({ + width: initialWidth, + height: initialHeight, + }); + const containerRef = useRef(null); + + const updateDimensions = useCallback( + (newWidth: number, newHeight: number) => { + setDimensions((prevDimensions) => ({ + width: + resizeDirection === "x" || resizeDirection === "both" + ? `${newWidth}px` + : prevDimensions.width, + height: + resizeDirection === "y" || resizeDirection === "both" + ? `${newHeight}px` + : prevDimensions.height, + })); + }, + [resizeDirection] + ); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const handleResize = (e: MouseEvent) => { + const { clientX, clientY } = e; + const { left, top, width, height } = container.getBoundingClientRect(); + + let newWidth = width; + let newHeight = height; + + // Adjusting the condition to check if the mouse is near the border, not just at the border + const isNearRightBorder = clientX > left + width - 20; // 20px for the grab area + const isNearBottomBorder = clientY > top + height - 20; // 20px for the grab area + + if ( + (resizeDirection === "x" || resizeDirection === "both") && + isNearRightBorder + ) { + newWidth = Math.max(100, clientX - left); // Adjusted to not add extra space + } + if ( + (resizeDirection === "y" || resizeDirection === "both") && + isNearBottomBorder + ) { + newHeight = Math.max(100, clientY - top); // Adjusted to not add extra space + } + + updateDimensions(newWidth, newHeight); + }; + + const startResizing = (e: MouseEvent) => { + e.preventDefault(); + document.addEventListener("mousemove", handleResize); + document.addEventListener("mouseup", stopResizing); + }; + + const stopResizing = () => { + document.removeEventListener("mousemove", handleResize); + document.removeEventListener("mouseup", stopResizing); + }; + + container.addEventListener("mousedown", startResizing); + + return () => { + container.removeEventListener("mousedown", startResizing); + }; + }, [resizeDirection, updateDimensions]); + + return ( +
+ {React.Children.map(children, (child, index) => { + return React.cloneElement(child as React.ReactElement, { key: index }); + })} +
+ ); +}; + +export default ResizableContainer; diff --git a/playground/src/pages/index.tsx b/playground/src/pages/index.tsx index eb11134..df52add 100644 --- a/playground/src/pages/index.tsx +++ b/playground/src/pages/index.tsx @@ -7,6 +7,7 @@ import useSimulation from "@/hooks/useRunSimulation"; import { Inter } from "next/font/google"; import { useEffect, useState } from "react"; import { motion } from "framer-motion"; +import ResizableContainer from "@/components/resizebale-container"; const inter = Inter({ subsets: ["latin"] }); @@ -64,7 +65,7 @@ export default function Home() { }, []); return ( -
+
@@ -89,19 +90,35 @@ export default function Home() {
-
-
+
+ -
-
+ +
-
-

Simulation Logs:

-

{simulationOutcome}

+ +
+

+ Simulation Logs: +

+ +

+ {simulationOutcome} +

-
+
); diff --git a/setup.py b/setup.py index 865fb57..5d2ff49 100644 --- a/setup.py +++ b/setup.py @@ -16,13 +16,15 @@ include_package_data=True, package_dir={"": "src"}, package_data={ - "simulatrex": ["llm_utils/prompt_templates/*.txt"], + "simulatrex": ["llms/prompts/prompt_templates/**/*.txt"], }, install_requires=[ + "pandas", + "numpy", + "chromadb", "openai", "uuid", "termcolor", - "chromadb", "pydantic", "python-dotenv", "requests", diff --git a/src/simulatrex/__init__.py b/src/simulatrex/__init__.py index 3b30249..7173ff8 100644 --- a/src/simulatrex/__init__.py +++ b/src/simulatrex/__init__.py @@ -4,7 +4,9 @@ ROOT_DIR = os.path.dirname(BASE_DIR) from simulatrex.config import Config, SETTINGS -from simulatrex.agents.agent import Agent -from simulatrex.agents.agent_group import AgentGroup +from simulatrex.agents.generative_agent import GenerativeAgent from simulatrex.experiments.scenarios.scenario import Scenario from simulatrex.experiments.surveys.survey import Survey +from simulatrex.environment import Environment +from simulatrex.simulation import Simulation +from simulatrex.dsl_parser import parse_dsl diff --git a/src/simulatrex/agents/__init__.py b/src/simulatrex/agents/__init__.py index a3ae8d9..6f9d6bb 100644 --- a/src/simulatrex/agents/__init__.py +++ b/src/simulatrex/agents/__init__.py @@ -1,2 +1,2 @@ -from simulatrex.agents.agent import Agent +from simulatrex.agents.generative_agent import GenerativeAgent from simulatrex.agents.agent_group import AgentGroup diff --git a/src/simulatrex/agents/agent.py b/src/simulatrex/agents/agent.py deleted file mode 100644 index 3efe882..0000000 --- a/src/simulatrex/agents/agent.py +++ /dev/null @@ -1,103 +0,0 @@ -""" -Author: Dominik Scherm (dom@simulatrex.ai) - -File: agent.py -Description: Agent class for the simulation - -""" - -from __future__ import annotations -import copy -import inspect -import types -from typing import Any, Callable, Optional, Union, Dict - -from rich.table import Table -from simulatrex.agents.types import AgentResponse - -from simulatrex.base import Base -from simulatrex.experiments.questions.question import Question -from simulatrex.llms.llm_base import LanguageModel -from simulatrex.utils.sync import sync_wrapper -from simulatrex.utils.errors import AgentCombinationError - - -class Agent(Base): - """An agent represents a human in the simulation.""" - - default_instruction = """Respond to each question by fully assuming a human persona. Maintain this character consistently and without deviation.""" - - traits = {} - rules = {} - instructions = {} - - def __init__( - self, - traits: dict = None, - rules: dict = None, - instructions: str = None, - ): - self._traits = traits or dict() - self.rules = rules or dict() - self.instructions = instructions or self.default_instruction - self.current_question = None - - def set_answer_method(self, method: Callable): - """Adds a method to the agent that can answer a particular question type.""" - - signature = inspect.signature(method) - for argument in ["question", "scenario", "self"]: - if argument not in signature.parameters: - raise Exception( - f"The method {method} does not have a '{argument}' parameter." - ) - bound_method = types.MethodType(method, self) - setattr(self, "answer_question_directly", bound_method) - - def __add__(self, other_agent: Agent = None) -> Agent: - """ - Combines two agents by joining their traits - """ - if other_agent is None: - return self - elif common_traits := set(self.traits.keys()) & set(other_agent.traits.keys()): - raise AgentCombinationError( - f"The agents have overlapping traits: {common_traits}." - ) - else: - new_agent = Agent(traits=copy.deepcopy(self.traits)) - new_agent.traits.update(other_agent.traits) - return new_agent - - def __eq__(self, other: Agent) -> bool: - """Checks if two agents are equal. Only checks the traits.""" - return self.data == other.data - - def __repr__(self): - class_name = self.__class__.__name__ - items = [ - f"{k} = '{v}'" if isinstance(v, str) else f"{k} = {v}" - for k, v in self.data.items() - if k != "question_type" - ] - return f"{class_name}({', '.join(items)})" - - @property - def data(self): - raw_data = { - k.replace("_", "", 1): v - for k, v in self.__dict__.items() - if k.startswith("_") - } - if self.rules == {}: - raw_data.pop("instructions") - return raw_data - - def to_dict(self) -> dict[str, Union[dict, bool]]: - """Serializes to a dictionary.""" - return self.data - - @classmethod - def from_dict(cls, agent_dict: dict[str, Union[dict, bool]]) -> Agent: - """Deserializes from a dictionary.""" - return cls(**agent_dict) diff --git a/src/simulatrex/agents/agent_group.py b/src/simulatrex/agents/agent_group.py index a041f99..5258ef8 100644 --- a/src/simulatrex/agents/agent_group.py +++ b/src/simulatrex/agents/agent_group.py @@ -8,12 +8,12 @@ from typing import List, Optional -from simulatrex.agents.agent import Agent +from simulatrex.agents.generative_agent import GenerativeAgent from simulatrex.base import Base class AgentGroup(Base, list): - def __init__(self, agents: Optional[List[Agent]] = None): + def __init__(self, agents: Optional[List[GenerativeAgent]] = None): super().__init__() if agents is not None: self.extend(agents) @@ -24,5 +24,5 @@ def to_dict(self) -> dict: @classmethod def from_dict(cls, data: dict) -> "AgentGroup": agent_data = data.get("agent_group", []) - agents = [Agent.from_dict(agent_dict) for agent_dict in agent_data] + agents = [GenerativeAgent.from_dict(agent_dict) for agent_dict in agent_data] return cls(agents) diff --git a/src/simulatrex/agents/target_audience.py b/src/simulatrex/agents/generativ_audiences.py similarity index 95% rename from src/simulatrex/agents/target_audience.py rename to src/simulatrex/agents/generativ_audiences.py index 6299bb5..283c8df 100644 --- a/src/simulatrex/agents/target_audience.py +++ b/src/simulatrex/agents/generativ_audiences.py @@ -1,8 +1,8 @@ """ Author: Dominik Scherm (dom@simulatrex.ai) -File: target_audience.py -Description: Target Audience Class +File: generative_audiences.py +Description: Defines the generative audience class for the simulation """ diff --git a/src/simulatrex/agents/generative_agent.py b/src/simulatrex/agents/generative_agent.py new file mode 100644 index 0000000..11ebb21 --- /dev/null +++ b/src/simulatrex/agents/generative_agent.py @@ -0,0 +1,85 @@ +""" +Author: Dominik Scherm (dom@simulatrex.ai) + +File: generative_agent.py +Description: Generative Agent class for the simulation + +""" + +from __future__ import annotations +import copy +import logging +from typing import Union + +from simulatrex.associative_memory import associate_memory +from simulatrex.llms.models import OpenAILanguageModel +from simulatrex.types.agent import ActionSpec, AgentResponse + +from simulatrex.base import Base +from simulatrex.experiments.questions.question import Question +from simulatrex.types.model import LanguageModelId +from simulatrex.utils.errors import AgentCombinationError + + +class GenerativeAgent(Base): + """An agent represents a human in the simulation.""" + + default_instruction = """Respond to each question by fully assuming a human persona. Maintain this character consistently and without deviation.""" + + attributes = {} + rules = {} + instructions = {} + + def __init__( + self, + identifier: str, + attributes: dict = None, + instructions: str = None, + llm_model_id: LanguageModelId = LanguageModelId.GPT_4_TURBO, + user_controlled: bool = False, + logger=logging.getLogger("simulation_logger"), + verbose: bool = False, + ): + self.identifier = identifier + self.attributes = attributes or dict() + self.instructions = instructions or self.default_instruction + + self._llm = OpenAILanguageModel(llm_model_id) if llm_model_id else None + self._user_controlled = user_controlled + self._logger = logger + self._verbose = verbose + + @property + def id(self) -> str: + return self.identifier + + def copy(self) -> GenerativeAgent: + """Creates a copy of the agent.""" + return copy.deepcopy(self) + + async def act(self, actions: ActionSpec) -> AgentResponse: + """Generates a response to a question.""" + if self._llm: + for action in actions: + response = await self._llm.ask(f"Generate an action for: {action}.") + self._logger.info( + f"Agent {self.identifier} action for {action}: {response}" + ) + else: + raise ValueError(f"Agent {self.identifier} has no LLM to generate action.") + + def to_dict(self) -> dict[str, Union[dict, bool]]: + """Serializes to a dictionary.""" + return { + "identifier": self.identifier, + "attributes": self.attributes, + "instructions": self.instructions, + "llm_model_id": self._llm.model_id, + "user_controlled": self._user_controlled, + "verbose": self._verbose, + } + + @classmethod + def from_dict(cls, agent_dict: dict[str, Union[dict, bool]]) -> GenerativeAgent: + """Deserializes from a dictionary.""" + return cls(**agent_dict) diff --git a/src/simulatrex/agents/types.py b/src/simulatrex/agents/types.py deleted file mode 100644 index d04ef69..0000000 --- a/src/simulatrex/agents/types.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -Author: Dominik Scherm (dom@simulatrex.ai) - -File: types.py -Description: AgentResponse Class - -""" - -from collections import UserDict - - -class AgentResponse(UserDict): - def __init__(self, *, question_name, answer, comment, prompts): - super().__init__( - { - "question_name": question_name, - "answer": answer, - "comment": comment, - "prompts": prompts, - } - ) diff --git a/src/simulatrex/llms/models/__init__.py b/src/simulatrex/associative_memory/__init__.py similarity index 100% rename from src/simulatrex/llms/models/__init__.py rename to src/simulatrex/associative_memory/__init__.py diff --git a/src/simulatrex/associative_memory/associate_memory.py b/src/simulatrex/associative_memory/associate_memory.py new file mode 100644 index 0000000..b538af6 --- /dev/null +++ b/src/simulatrex/associative_memory/associate_memory.py @@ -0,0 +1,272 @@ +""" +Author: Dominik Scherm (dom@simulatrex.ai) + +File: associate_memory.py +Description: An associative memory for the simulation + +""" + +"""An associative memory similar to the one in the following paper. + +Park, J.S., O'Brien, J.C., Cai, C.J., Morris, M.R., Liang, P. and Bernstein, +M.S., 2023. Generative agents: Interactive simulacra of human behavior. arXiv +preprint arXiv:2304.03442. +""" +from collections.abc import Callable, Iterable +import datetime +import typing + +import numpy as np +import pandas as pd + + +class AssociativeMemory: + """Class that implements associative memory.""" + + def __init__( + self, + text_embedding_model: Callable[[str], np.ndarray], + importance: Callable[[str], float], + ): + """Constructor. + + Args: + text_embedding_model: text embedding model + importance_scale: maps a sentence into [0,1] scale of importance + """ + # self._memory_bank_lock = threading.Lock() + self._embedder = text_embedding_model + self._importance = importance + + self._memory_bank = pd.DataFrame( + columns=["text", "time", "tags", "embedding", "importance"] + ) + + def add( + self, + text: str, + *, + timestamp: typing.Optional[datetime.datetime] = None, + tags: typing.Optional[list[str]] = None, + importance: typing.Optional[float] = None, + ): + """Adds the text to the memory. + + Args: + text: what goes into the memory + timestamp: the time of the memory + tags: optional tags + importance: optionally set the importance of the memory. + """ + + embedding = self._embedder(text) + if importance is None: + importance = self._importance(text) + + new_df = ( + pd.Series( + { + "text": text, + "time": timestamp, + "tags": tags, + "embedding": embedding, + "importance": importance, + } + ) + .to_frame() + .T + ) + + with self._memory_bank_lock: + self._memory_bank = pd.concat( + [self._memory_bank, new_df], ignore_index=True + ) + + def extend( + self, + texts: Iterable[str], + **kwargs, + ): + """Adds the texts to the memory. + + Args: + texts: list of strings to add to the memory + **kwargs: arguments to pass on to .add + """ + for text in texts: + self.add(text, **kwargs) + + def get_data_frame(self): + with self._memory_bank_lock: + return self._memory_bank.copy() + + def _get_top_k_cosine(self, x: np.ndarray, k: int): + """Returns the top k most cosine similar rows to an input vector x. + + Args: + x: The input vector. + k: The number of rows to return. + + Returns: + Rows, sorted by cosine similarity in descending order. + """ + with self._memory_bank_lock: + cosine_similarities = self._memory_bank["embedding"].apply( + lambda y: np.dot(x, y) + ) + + # Sort the cosine similarities in descending order. + cosine_similarities.sort_values(ascending=False, inplace=True) + + # Return the top k rows. + return self._memory_bank.iloc[cosine_similarities.head(k).index] + + def _get_top_k_similar_rows( + self, x, k: int, use_recency: bool = True, use_importance: bool = True + ): + """Returns the top k most similar rows to an input vector x. + + Args: + x: The input vector. + k: The number of rows to return. + use_recency: if true then weight similarity by recency + use_importance: if true then weight similarity by importance + + Returns: + Rows, sorted by cosine similarity in descending order. + """ + with self._memory_bank_lock: + cosine_similarities = self._memory_bank["embedding"].apply( + lambda y: np.dot(x, y) + ) + + similarity_score = cosine_similarities + + if use_recency: + max_time = self._memory_bank["time"].max() + discounted_time = self._memory_bank["time"].apply( + lambda y: 0.99 ** ((max_time - y) / datetime.timedelta(minutes=1)) + ) + similarity_score += discounted_time + + if use_importance: + importance = self._memory_bank["importance"] + similarity_score += importance + + # Sort the similarities in descending order. + similarity_score.sort_values(ascending=False, inplace=True) + + # Return the top k rows. + return self._memory_bank.iloc[similarity_score.head(k).index] + + def _get_k_recent(self, k: int): + with self._memory_bank_lock: + recency = self._memory_bank["time"].sort_values(ascending=False) + return self._memory_bank.iloc[recency.head(k).index] + + def _pd_to_text( + self, + data: pd.DataFrame, + add_time: bool = False, + sort_by_time: bool = True, + ): + """Formats a dataframe into list of strings. + + Args: + data: the dataframe to process + add_time: whether to add time + sort_by_time: whether to sort by time + + Returns: + A list of strings, one for each memory + """ + if sort_by_time: + data = data.sort_values("time", ascending=True) + + if add_time and not data.empty: + if self._interval: + this_time = data["time"] + next_time = data["time"] + self._interval + + interval = this_time.dt.strftime( + "%d %b %Y [%H:%M:%S " + ) + next_time.dt.strftime("- %H:%M:%S]: ") + output = interval + data["text"] + else: + output = data["time"].dt.strftime("[%d %b %Y %H:%M:%S] ") + data["text"] + else: + output = data["text"] + + return output.tolist() + + def retrieve_associative( + self, + query: str, + k: int = 1, + use_recency: bool = True, + use_importance: bool = True, + add_time: bool = True, + sort_by_time: bool = True, + ): + """Retrieve memories associatively. + + Args: + query: a string to use for retrieval + k: how many memories to retrieve + use_recency: whether to use timestamps to weight by recency or not + use_importance: whether to use importance for retrieval + add_time: whether to add time stamp to the output + sort_by_time: whether to sort the result by time + + Returns: + List of strings corresponding to memories + """ + query_embedding = self._embedder(query) + + data = self._get_top_k_similar_rows( + query_embedding, + k, + use_recency=use_recency, + use_importance=use_importance, + ) + + return self._pd_to_text(data, add_time=add_time, sort_by_time=sort_by_time) + + def retrieve_recent( + self, + k: int = 1, + add_time: bool = False, + ): + """Retrieve memories by recency. + + Args: + k: number of entries to retrieve + add_time: whether to add time stamp to the output + + Returns: + List of strings corresponding to memories + """ + data = self._get_k_recent(k) + + return self._pd_to_text(data, add_time=add_time, sort_by_time=True) + + def retrieve_recent_with_importance( + self, + k: int = 1, + add_time: bool = False, + ): + """Retrieve memories by recency and return importance alongside. + + Args: + k: number of entries to retrieve + add_time: whether to add time stamp to the output + + Returns: + List of strings corresponding to memories + """ + data = self._get_k_recent(k) + + return ( + self._pd_to_text(data, add_time=add_time, sort_by_time=True), + list(data["importance"]), + ) diff --git a/src/simulatrex/db.py b/src/simulatrex/db.py index 867f02a..c727032 100644 --- a/src/simulatrex/db.py +++ b/src/simulatrex/db.py @@ -5,17 +5,14 @@ Description: Database utils """ + import os import uuid from sqlalchemy import Column, String, Float, Integer, create_engine from sqlalchemy.orm import sessionmaker, declarative_base -from simulatrex.utils.log import SingletonLogger - Base = declarative_base() -_logger = SingletonLogger - class MemoryUnitDB(Base): __tablename__ = "memory_units" diff --git a/src/simulatrex/dsl_parser.py b/src/simulatrex/dsl_parser.py index 64ec58f..7ecb121 100644 --- a/src/simulatrex/dsl_parser.py +++ b/src/simulatrex/dsl_parser.py @@ -1,4 +1,6 @@ -from simulatrex.simulation_entities import Agent, Simulation, Environment +from simulatrex.agents.generative_agent import GenerativeAgent +from simulatrex.simulation import Simulation +from simulatrex.environment import Environment from ply import lex, yacc # List of token names. This is always required @@ -8,7 +10,6 @@ "SIMULATION", "IDENTIFIER", "NUMBER", - "ACTIONS", "ATTRIBUTES", "ENTITIES", "EPOCHS", @@ -58,11 +59,6 @@ def t_SIMULATION(t): return t -def t_ACTIONS(t): - r"Actions" - return t - - def t_ATTRIBUTES(t): r"Attributes" return t @@ -109,11 +105,10 @@ def p_simulation_entity(p): def p_agent_definition(p): - """agent_definition : AGENT COLON IDENTIFIER attributes actions""" + """agent_definition : AGENT COLON IDENTIFIER attributes""" agent_identifier = p[3] agent_attributes = p[4] - agent_actions = p[5] - p[0] = Agent(agent_identifier, agent_attributes, agent_actions) + p[0] = GenerativeAgent(agent_identifier, agent_attributes) def p_environment_definition(p): @@ -136,11 +131,6 @@ def p_attributes(p): p[0] = p[3] -def p_actions(p): - """actions : ACTIONS COLON list_of_identifiers""" - p[0] = p[3] - - def p_entities(p): """entities : ENTITIES COLON list_of_identifiers""" p[0] = p[3] @@ -208,7 +198,7 @@ def parse_dsl(data): for item in parsed_data: if isinstance(item, Simulation) and simulation is None: simulation = item - elif isinstance(item, Agent): + elif isinstance(item, GenerativeAgent): agents.append(item) elif isinstance(item, Environment) and environment is None: environment = item diff --git a/src/simulatrex/environment.py b/src/simulatrex/environment.py new file mode 100644 index 0000000..986cd68 --- /dev/null +++ b/src/simulatrex/environment.py @@ -0,0 +1,34 @@ +""" +Author: Dominik Scherm (dom@simulatrex.ai) + +File: environment.py +Description: Environment class for the simulation + +""" + +import logging + +from simulatrex.agents.generative_agent import GenerativeAgent + + +class Environment: + def __init__( + self, + identifier: str, + entities: list[str] = None, + logger=logging.getLogger("simulation_logger"), + ): + self.identifier = identifier + self.entities = entities if entities is not None else [] + + self._logger = logger + + async def interact(self, agents: list[GenerativeAgent]): + self._logger.info( + f"Environment {self.identifier} is facilitating interaction among agents." + ) + for agent in agents: + await agent.act() + + def to_dict(self): + return {"id": self.identifier, "entities": self.entities} diff --git a/src/simulatrex/llms/llm_base.py b/src/simulatrex/llms/llm_base.py deleted file mode 100644 index 3243a0f..0000000 --- a/src/simulatrex/llms/llm_base.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -Author: Dominik Scherm (dom@simulatrex.ai) - -File: llm_base.py -Description: Language Model Base Class - -""" - -from __future__ import annotations -from functools import wraps -import json -from abc import ABC, abstractmethod - -from typing import Any -from simulatrex.utils.errors import ( - LanguageModelResponseNotJSONError, -) - - -class LanguageModel(ABC): - """ABC for LLM subclasses.""" - - _model_ = None - - def __init__(self, **kwargs): - self.model = getattr(self, "_model_", None) - default_parameters = getattr(self, "_parameters_", None) - parameters = self._overide_default_parameters(kwargs, default_parameters) - self.parameters = parameters - - for key, value in parameters.items(): - setattr(self, key, value) - - for key, value in kwargs.items(): - if key not in parameters: - setattr(self, key, value) - - @staticmethod - def _overide_default_parameters(passed_parameter_dict, default_parameter_dict): - """Returns a dictionary of parameters, with passed parameters taking precedence over defaults.""" - parameters = dict({}) - for parameter, default_value in default_parameter_dict.items(): - if parameter in passed_parameter_dict: - parameters[parameter] = passed_parameter_dict[parameter] - else: - parameters[parameter] = default_value - return parameters - - @abstractmethod - async def async_execute_model_call(): - pass - - def _save_response_to_db(self, prompt, system_prompt, response): - try: - output = json.dumps(response) - except json.JSONDecodeError: - raise LanguageModelResponseNotJSONError - self.crud.write_LLMOutputData( - model=str(self.model), - parameters=str(self.parameters), - system_prompt=system_prompt, - prompt=prompt, - output=output, - ) - - def to_dict(self) -> dict[str, Any]: - """Converts instance to a dictionary.""" - return {"model": self.model, "parameters": self.parameters} - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(model = '{self.model}', parameters={self.parameters})" diff --git a/src/simulatrex/llms/models/models.py b/src/simulatrex/llms/models.py similarity index 95% rename from src/simulatrex/llms/models/models.py rename to src/simulatrex/llms/models.py index 59c4a97..3e43757 100644 --- a/src/simulatrex/llms/models/models.py +++ b/src/simulatrex/llms/models.py @@ -15,9 +15,8 @@ from dotenv import load_dotenv from pydantic import BaseModel from openai import OpenAI -import requests import instructor -from simulatrex.llms.types import LanguageModel +from simulatrex.types.model import LanguageModelId load_dotenv() @@ -51,7 +50,7 @@ class OpenAILanguageModel(BaseLanguageModel): This is a wrapper for the OpenAI API. """ - def __init__(self, model_id=LanguageModel.GPT_4, agent_id=None): + def __init__(self, model_id=LanguageModelId.GPT_4, agent_id=None): api_key = os.environ.get("OPENAI_API_KEY") if api_key is None: @@ -137,7 +136,7 @@ class LlamaLanguageModel(BaseLanguageModel): This class is a wrapper for the LLama API. """ - def __init__(self, model_id=LanguageModel.LLAMA_2_70B_CHAT_HF, agent_id=None): + def __init__(self, model_id=LanguageModelId.LLAMA_2_70B_CHAT_HF, agent_id=None): access_token = os.environ.get("HUGGINGFACE_ACCESS_TOKEN") if access_token is None: @@ -150,7 +149,7 @@ def __init__(self, model_id=LanguageModel.LLAMA_2_70B_CHAT_HF, agent_id=None): self.agent_id = agent_id async def ask(self, prompt: str) -> str: - if self.model_id == LanguageModel.LLAMA_2_70B_CHAT_HF: + if self.model_id == LanguageModelId.LLAMA_2_70B_CHAT_HF: try: API_URL = "https://api-inference.huggingface.co/models/meta-llama/Llama-2-70b-chat-hf" headers = {"Authorization": f"Bearer {self.access_token}"} diff --git a/src/simulatrex/manager.py b/src/simulatrex/manager.py index f617e42..dc44c89 100644 --- a/src/simulatrex/manager.py +++ b/src/simulatrex/manager.py @@ -1,5 +1,5 @@ from contextlib import asynccontextmanager -from .simulation_entities import Simulation +from .simulation import Simulation @asynccontextmanager diff --git a/src/simulatrex/simulation.py b/src/simulatrex/simulation.py new file mode 100644 index 0000000..09103cd --- /dev/null +++ b/src/simulatrex/simulation.py @@ -0,0 +1,65 @@ +""" +Author: Dominik Scherm (dom@simulatrex.ai) + +File: simulation.py +Description: Simulation class for the simulation + +""" + +import logging + +from simulatrex.agents.generative_agent import GenerativeAgent +from simulatrex.environment import Environment + + +class Simulation: + def __init__( + self, + identifier: str, + epochs: int = 1, + interactions: list[str] = None, + agents: list[GenerativeAgent] = [], + environment: Environment = None, + logger: logging.Logger = None, + ): + self.identifier = identifier + self.epochs = epochs + self.interactions = interactions if interactions is not None else [] + + self._agents = agents if agents is not None else [] + self._environment = environment + self._logger = ( + logger if logger is not None else logging.getLogger("simulation_logger") + ) + + @property + def agents(self): + return self._agents + + @agents.setter + def agents(self, value): + self._agents = value + + @property + def environment(self): + return self._environment + + @environment.setter + def environment(self, value): + self._environment = value + + async def run(self): + self._logger.info(f"Simulation {self.identifier} started.") + for _ in range(self.epochs): + if self.environment: + await self.environment.interact(self.agents) + for interaction in self.interactions: + self._logger.info(f"Executing interaction: {interaction}") + self._logger.info(f"Simulation {self.identifier} ended.") + + def to_dict(self): + return { + "id": self.identifier, + "epochs": self.epochs, + "interactions": self.interactions, + } diff --git a/src/simulatrex/simulation_entities.py b/src/simulatrex/simulation_entities.py deleted file mode 100644 index 6dcd07b..0000000 --- a/src/simulatrex/simulation_entities.py +++ /dev/null @@ -1,114 +0,0 @@ -import logging -from simulatrex.llms.models.models import OpenAILanguageModel -from simulatrex.llms.types import LanguageModel - - -class Agent: - def __init__( - self, - identifier, - attributes=None, - actions=None, - llm_id=LanguageModel.GPT_4_TURBO, - logger=logging.getLogger("simulation_logger"), - ): - self.identifier = identifier - self.attributes = attributes if attributes is not None else {} - self.actions = actions if actions is not None else [] - self.llm = OpenAILanguageModel(model_id=llm_id) if llm_id else None - self.logger = logger - - async def act(self): - if self.llm: - for action in self.actions: - response = await self.llm.ask(f"Generate an action for: {action}.") - self.logger.info( - f"Agent {self.identifier} action for {action}: {response}" - ) # Use logger - else: - self.logger.warning( - f"Agent {self.identifier} has no LLM to generate action." - ) # Use logger - - def to_dict(self): - return { - "id": self.identifier, - "attributes": self.attributes, - "actions": self.actions, - } - - -class Environment: - def __init__( - self, identifier, entities=None, logger=logging.getLogger("simulation_logger") - ): - self.identifier = identifier - self.entities = entities if entities is not None else [] - self.logger = logger # Store logger - - async def interact(self, agents): - self.logger.info( # Use logger - f"Environment {self.identifier} is facilitating interaction among agents." - ) - for agent in agents: - await agent.act() - for entity in self.entities: - self.logger.debug(f"Interacting with entity: {entity}") - - def to_dict(self): - return { - "id": self.identifier, - "entities": self.entities, - } - - -class Simulation: - def __init__( - self, - identifier, - epochs=1, - interactions=None, - agents=None, - environment=None, - logger=None, - ): - self.identifier = identifier - self.epochs = epochs - self.interactions = interactions if interactions is not None else [] - self._agents = agents if agents is not None else [] - self._environment = environment - self.logger = ( - logger if logger is not None else logging.getLogger("simulation_logger") - ) - - @property - def agents(self): - return self._agents - - @agents.setter - def agents(self, value): - self._agents = value - - @property - def environment(self): - return self._environment - - @environment.setter - def environment(self, value): - self._environment = value - - async def run(self): - self.logger.info(f"Simulation {self.identifier} started.") - for _ in range(self.epochs): - if self.environment: - await self.environment.interact(self.agents) - for interaction in self.interactions: - self.logger.info(f"Executing interaction: {interaction}") - self.logger.info(f"Simulation {self.identifier} ended.") - - def to_dict(self): - return { - "id": self.identifier, - "epochs": self.epochs, - "interactions": self.interactions, - } diff --git a/src/simulatrex/types.py b/src/simulatrex/types.py deleted file mode 100644 index b825961..0000000 --- a/src/simulatrex/types.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -Author: Dominik Scherm (dom@simulatrex.ai) - -File: config.py -Description: Defines a config for a simulation - -""" - -# Outdated, to be removed - -from pydantic import BaseModel -from typing import List, Dict, Optional, Union - - -class Objective(BaseModel): - id: str - description: str - metric: Optional[str] = None - target: str - - -class ResponseModel(BaseModel): - type: str - parameters: Dict[str, float] - - -class AgentIdentity(BaseModel): - name: str - age: Optional[int] = None - gender: Optional[str] = None - ethnicity: Optional[str] = None - language: Optional[str] = None - persona: Optional[str] = None - personality_description: Optional[str] = None - traits: List[str] = [] - interests: List[str] = [] - knowledge_base: List[str] = [] - skills: List[str] = [] - behavior_patterns: List[str] = [] - past_experiences: List[str] = [] - societal_role: Optional[str] = None - affiliations: List[str] = [] - current_state: Optional[str] = None - core_memories: List[str] = [] - - -class InitialConditions(BaseModel): - awareness: Optional[float] = None - - -class TargetGroupRelationship(BaseModel): - target_group_id: str # ID of the other agent in this relationship - type: str # E.g., "friend", "colleague" - strength: float # E.g., from 0 (acquaintance) to 1 (best friend) - - -class AgentRelationship(BaseModel): - agent_id: str # ID of the other agent in this relationship - type: str # E.g., "friend", "colleague" - strength: float # E.g., from 0 (acquaintance) to 1 (best friend) - - def summary(self) -> str: - return f"{self.type.capitalize()} with {self.agent_id} at a strength level of {self.strength}" - - -class AgentGroup(BaseModel): - id: str - type: str # E.g., "school", "company" - member_agent_ids: List[str] - metadata: Optional[Dict[str, Union[str, int, float]]] - - -class Agent(BaseModel): - id: str - type: str - identity: AgentIdentity - initial_conditions: InitialConditions - cognitive_model: str - relationships: List[AgentRelationship] - group_affiliations: List[str] - - -class AgentsHierarchy(BaseModel): - organizations: List[Dict[str, Union[str, List[str]]]] - - -class MappingRule(BaseModel): - csv_column: str - agent_attribute: str - - -class DataFeed(BaseModel): - type: str - location: str - mapping_rules: List[MappingRule] - - -class SimulationTimeConfig(BaseModel): - start_time: str - end_time: str - time_multiplier: int - - -class Environment(BaseModel): - type: str - description: str - context: str - entities: List[str] - time_config: SimulationTimeConfig - # data_feed: Union[None, DataFeed] - - -class Event(BaseModel): - id: str - type: str - source: str - content: str - impact: Optional[float] = None - scheduled_time: str - - -class Evaluation(BaseModel): - metrics: List[str] - objectives: List[Objective] - - -class ExpectedOutcome(BaseModel): - average_awareness_level: float - highest_influence_platform: str - agent_collaboration_count: int - - -class TargetGroup(BaseModel): - id: str - role: str - responsibilities: str - initial_conditions: Optional[InitialConditions] = None - num_agents: int = 1 - relationships: Optional[List[TargetGroupRelationship]] = None - - -class SimulationConfig(BaseModel): - title: str - environment: Environment - agents: Optional[List[Agent]] = None - groups: Optional[List[AgentGroup]] = None - target_groups: Optional[List[TargetGroup]] = None - events: List[Event] - evaluation: Evaluation - - -class Config(BaseModel): - version: str - simulation: SimulationConfig diff --git a/src/simulatrex/types/__init__.py b/src/simulatrex/types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/simulatrex/types/agent.py b/src/simulatrex/types/agent.py new file mode 100644 index 0000000..d575a2f --- /dev/null +++ b/src/simulatrex/types/agent.py @@ -0,0 +1,128 @@ +""" +Author: Dominik Scherm (dom@simulatrex.ai) + +File: agent.py +Description: Agent type defintions for the simulation + +""" + +from typing import List, Dict, Optional, Union +from collections import UserDict + +from pydantic import BaseModel +from simulatrex.base import Base + + +class ActionSpec(Base): + """A specification of the action that agent is queried for. + + Attributes: + call_to_action: formatted text that conditions agents response. {agent_id} + and {timedelta} will be inserted by the agent. + output_type: type of output - FREE, CHOICE, or FLOAT + options: if multiple choice, then provide possible answers here + tag: a tag to add to the activity memory (e.g., action, speech, etc.) + """ + + def __init__( + self, + call_to_action: str, + output_type: str, + options: Optional[List[str]] = None, + tag: Optional[str] = None, + ): + self.call_to_action = call_to_action + self.output_type = output_type + self.options = options + self.tag = tag + + @classmethod + def from_dict(cls, data: Dict[str, Union[str, List[str], None]]) -> "ActionSpec": + """Creates an instance of ActionSpec from a dictionary.""" + return cls( + call_to_action=data.get("call_to_action", ""), + output_type=data.get("output_type", "FREE"), + options=data.get("options", None), + tag=data.get("tag", None), + ) + + def to_dict(self) -> Dict[str, Union[str, List[str], None]]: + """Serializes the instance to a dictionary.""" + return { + "call_to_action": self.call_to_action, + "output_type": self.output_type, + "options": self.options, + "tag": self.tag, + } + + +OUTPUT_TYPES = ["FREE", "CHOICE", "FLOAT"] + +DEFAULT_CALL_TO_SPEECH = ( + "Given the above, what is {agent_id} likely to say next? Respond in" + ' the format `{agent_id} -- "..."` For example, ' + 'Cristina -- "Hello! Mighty fine weather today, right?", ' + 'Ichabod -- "I wonder if the alfalfa is ready to harvest", or ' + 'Townsfolk -- "Good morning".\n' +) + +DEFAULT_CALL_TO_ACTION = ( + "What would {agent_id} do for the next {timedelta}? " + "Give a specific activity. Pick an activity that " + "would normally take about {timedelta} to complete. " + "If the selected action has a direct or indirect object then it " + "must be specified explicitly. For example, it is valid to respond " + 'with "{agent_id} votes for Caroline because..." but not ' + 'valid to respond with "{agent_id} votes because...".' +) + + +DEFAULT_ACTION_SPEC = ActionSpec( + DEFAULT_CALL_TO_ACTION, + "FREE", + options=None, + tag="action", +) + + +class Agent(BaseModel): + identifier: str + attributes: Dict[str, Union[str, int, float]] + actions: List[str] + instructions: str + model_id: str + memory: Optional[str] + user_controlled: bool + verbose: bool + + +class AgentRelationship(BaseModel): + agent_id: str # ID of the other agent in this relationship + type: str # E.g., "friend", "colleague" + strength: float # E.g., from 0 (acquaintance) to 1 (best friend) + + def summary(self) -> str: + return f"{self.type.capitalize()} with {self.agent_id} at a strength level of {self.strength}" + + +class AgentGroup(BaseModel): + id: str + type: str # E.g., "school", "company" + member_agent_ids: List[str] + metadata: Optional[Dict[str, Union[str, int, float]]] + + +class AgentsHierarchy(BaseModel): + organizations: List[Dict[str, Union[str, List[str]]]] + + +class AgentResponse(UserDict): + def __init__(self, *, question_name, answer, comment, prompts): + super().__init__( + { + "question_name": question_name, + "answer": answer, + "comment": comment, + "prompts": prompts, + } + ) diff --git a/src/simulatrex/llms/types.py b/src/simulatrex/types/model.py similarity index 94% rename from src/simulatrex/llms/types.py rename to src/simulatrex/types/model.py index 284972f..38bfdd7 100644 --- a/src/simulatrex/llms/types.py +++ b/src/simulatrex/types/model.py @@ -1,7 +1,7 @@ """ Author: Dominik Scherm (dom@simulatrex.ai) -File: types.py +File: model.py Description: Types for the LLMs """ @@ -10,7 +10,7 @@ from simulatrex.llms.utils.memory import LongTermMemory, ShortTermMemory -class LanguageModel(Enum): +class LanguageModelId(Enum): GPT_4_TURBO = "gpt-4-0125-preview" GPT_4 = "gpt-4" GPT_3_5_Turbo = "gpt-3.5-turbo" diff --git a/src/simulatrex/vectordb.py b/src/simulatrex/vectordb.py index c58eb2d..71bf0f3 100644 --- a/src/simulatrex/vectordb.py +++ b/src/simulatrex/vectordb.py @@ -5,8 +5,8 @@ Description: Defines the vectordb class, which is a wrapper around ChromaDB """ + import os -import shutil import chromadb from chromadb.utils import embedding_functions from dotenv import load_dotenv