diff --git a/README.md b/README.md index 0b66f9c8..93232047 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,21 @@ Welcome to Swarms - the future of AI, where we leverage the power of autonomous --- +[![GitHub issues](https://img.shields.io/github/issues/kyegomez/swarms)](https://github.com/kyegomez/swarms/issues) [![GitHub forks](https://img.shields.io/github/forks/kyegomez/swarms)](https://github.com/kyegomez/swarms/network) [![GitHub stars](https://img.shields.io/github/stars/kyegomez/swarms)](https://github.com/kyegomez/swarms/stargazers) [![GitHub license](https://img.shields.io/github/license/kyegomez/swarms)](https://github.com/kyegomez/swarms/blob/main/LICENSE)[![GitHub star chart](https://img.shields.io/github/stars/kyegomez/swarms?style=social)](https://star-history.com/#kyegomez/swarms) +[![Dependency Status](https://img.shields.io/librariesio/github/kyegomez/swarms)](https://libraries.io/github/kyegomez/swarms) [![Downloads](https://static.pepy.tech/badge/swarms/month)](https://pepy.tech/project/swarms) + +
+

+GitHub Contributors +GitHub Last Commit + +GitHub Issues +GitHub Pull Requests +
+

-[![GitHub issues](https://img.shields.io/github/issues/kyegomez/swarms)](https://github.com/kyegomez/swarms/issues) [![GitHub forks](https://img.shields.io/github/forks/kyegomez/swarms)](https://github.com/kyegomez/swarms/network) [![GitHub stars](https://img.shields.io/github/stars/kyegomez/swarms)](https://github.com/kyegomez/swarms/stargazers) [![GitHub license](https://img.shields.io/github/license/kyegomez/swarms)](https://github.com/kyegomez/swarms/blob/main/LICENSE)[![GitHub star chart](https://img.shields.io/github/stars/kyegomez/swarms?style=social)](https://star-history.com/#kyegomez/swarms) -[![Dependency Status](https://img.shields.io/librariesio/github/kyegomez/swarms)](https://libraries.io/github/kyegomez/swarms) [![Downloads](https://static.pepy.tech/badge/swarms/month)](https://pepy.tech/project/swarms) ### Share on Social Media @@ -279,3 +289,9 @@ Remember, our roadmap is a guide, and we encourage you to bring your own ideas a + + + + + + diff --git a/llm.py b/llm.py index 1906ca7c..26b64836 100644 --- a/llm.py +++ b/llm.py @@ -1,5 +1,5 @@ # example -from swarms.utils.llm import LLM +from swarms.agents.models.llm import LLM llm_instance = LLM(hf_repo_id="google/flan-t5-xl", hf_api_token="your_hf_api_token") result = llm_instance.run("Who won the FIFA World Cup in 1998?") print(result) \ No newline at end of file diff --git a/swarms/agents/models/__init__.py b/swarms/agents/models/__init__.py new file mode 100644 index 00000000..d8e59621 --- /dev/null +++ b/swarms/agents/models/__init__.py @@ -0,0 +1 @@ +from swarms.agents.models.llm import LLM \ No newline at end of file diff --git a/swarms/utils/llm.py b/swarms/agents/models/llm.py similarity index 100% rename from swarms/utils/llm.py rename to swarms/agents/models/llm.py diff --git a/swarms/agents/workers/generative_worker.py b/swarms/agents/workers/generative_worker.py new file mode 100644 index 00000000..af1d9bd1 --- /dev/null +++ b/swarms/agents/workers/generative_worker.py @@ -0,0 +1,555 @@ +import logging +import re +from datetime import datetime +from typing import Any, Dict, List, Optional, Tuple + +############ +from langchain.prompts import PromptTemplate +from langchain.retrievers import TimeWeightedVectorStoreRetriever +from langchain.schema import BaseMemory, Document + +from langchain.schema.language_model import BaseLanguageModel +from langchain.utils import mock_now +from langchain import LLMChain +from langchain.schema.language_model import BaseLanguageModel + + +logger = logging.getLogger(__name__) + + +####################### + +from pydantic import BaseModel, Field + + + +class WorkerSims(BaseMemory): + llm: BaseLanguageModel + """The core language model.""" + + memory_retriever: TimeWeightedVectorStoreRetriever + """The retriever to fetch related memories.""" + verbose: bool = False + + reflection_threshold: Optional[float] = None + """When aggregate_importance exceeds reflection_threshold, stop to reflect.""" + + current_plan: List[str] = [] + """The current plan of the agent.""" + + # A weight of 0.15 makes this less important than it + # would be otherwise, relative to salience and time + importance_weight: float = 0.15 + """How much weight to assign the memory importance.""" + + aggregate_importance: float = 0.0 # : :meta private: + """Track the sum of the 'importance' of recent memories. + + Triggers reflection when it reaches reflection_threshold.""" + + max_tokens_limit: int = 1200 # : :meta private: + # input keys + queries_key: str = "queries" + most_recent_memories_token_key: str = "recent_memories_token" + add_memory_key: str = "add_memory" + # output keys + relevant_memories_key: str = "relevant_memories" + relevant_memories_simple_key: str = "relevant_memories_simple" + most_recent_memories_key: str = "most_recent_memories" + now_key: str = "now" + reflecting: bool = False + + def chain(self, prompt: PromptTemplate) -> LLMChain: + return LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose) + + @staticmethod + def _parse_list(text: str) -> List[str]: + """Parse a newline-separated string into a list of strings.""" + lines = re.split(r"\n", text.strip()) + lines = [line for line in lines if line.strip()] # remove empty lines + return [re.sub(r"^\s*\d+\.\s*", "", line).strip() for line in lines] + + def _get_topics_of_reflection(self, last_k: int = 50) -> List[str]: + """Return the 3 most salient high-level questions about recent observations.""" + prompt = PromptTemplate.from_template( + "{observations}\n\n" + "Given only the information above, what are the 3 most salient " + "high-level questions we can answer about the subjects in the statements?\n" + "Provide each question on a new line." + ) + observations = self.memory_retriever.memory_stream[-last_k:] + observation_str = "\n".join( + [self._format_memory_detail(o) for o in observations] + ) + result = self.chain(prompt).run(observations=observation_str) + return self._parse_list(result) + + def _get_insights_on_topic( + self, topic: str, now: Optional[datetime] = None + ) -> List[str]: + """Generate 'insights' on a topic of reflection, based on pertinent memories.""" + prompt = PromptTemplate.from_template( + "Statements relevant to: '{topic}'\n" + "---\n" + "{related_statements}\n" + "---\n" + "What 5 high-level novel insights can you infer from the above statements " + "that are relevant for answering the following question?\n" + "Do not include any insights that are not relevant to the question.\n" + "Do not repeat any insights that have already been made.\n\n" + "Question: {topic}\n\n" + "(example format: insight (because of 1, 5, 3))\n" + ) + + related_memories = self.fetch_memories(topic, now=now) + related_statements = "\n".join( + [ + self._format_memory_detail(memory, prefix=f"{i+1}. ") + for i, memory in enumerate(related_memories) + ] + ) + result = self.chain(prompt).run( + topic=topic, related_statements=related_statements + ) + # TODO: Parse the connections between memories and insights + return self._parse_list(result) + + def pause_to_reflect(self, now: Optional[datetime] = None) -> List[str]: + """Reflect on recent observations and generate 'insights'.""" + if self.verbose: + logger.info("Character is reflecting") + new_insights = [] + topics = self._get_topics_of_reflection() + for topic in topics: + insights = self._get_insights_on_topic(topic, now=now) + for insight in insights: + self.add_memory(insight, now=now) + new_insights.extend(insights) + return new_insights + + def _score_memory_importance(self, memory_content: str) -> float: + """Score the absolute importance of the given memory.""" + prompt = PromptTemplate.from_template( + "On the scale of 1 to 10, where 1 is purely mundane" + + " (e.g., brushing teeth, making bed) and 10 is" + + " extremely poignant (e.g., a break up, college" + + " acceptance), rate the likely poignancy of the" + + " following piece of memory. Respond with a single integer." + + "\nMemory: {memory_content}" + + "\nRating: " + ) + score = self.chain(prompt).run(memory_content=memory_content).strip() + if self.verbose: + logger.info(f"Importance score: {score}") + match = re.search(r"^\D*(\d+)", score) + if match: + return (float(match.group(1)) / 10) * self.importance_weight + else: + return 0.0 + + def _score_memories_importance(self, memory_content: str) -> List[float]: + """Score the absolute importance of the given memory.""" + prompt = PromptTemplate.from_template( + "On the scale of 1 to 10, where 1 is purely mundane" + + " (e.g., brushing teeth, making bed) and 10 is" + + " extremely poignant (e.g., a break up, college" + + " acceptance), rate the likely poignancy of the" + + " following piece of memory. Always answer with only a list of numbers." + + " If just given one memory still respond in a list." + + " Memories are separated by semi colans (;)" + + "\Memories: {memory_content}" + + "\nRating: " + ) + scores = self.chain(prompt).run(memory_content=memory_content).strip() + + if self.verbose: + logger.info(f"Importance scores: {scores}") + + # Split into list of strings and convert to floats + scores_list = [float(x) for x in scores.split(";")] + + return scores_list + + def add_memories( + self, memory_content: str, now: Optional[datetime] = None + ) -> List[str]: + """Add an observations or memories to the agent's memory.""" + importance_scores = self._score_memories_importance(memory_content) + + self.aggregate_importance += max(importance_scores) + memory_list = memory_content.split(";") + documents = [] + + for i in range(len(memory_list)): + documents.append( + Document( + page_content=memory_list[i], + metadata={"importance": importance_scores[i]}, + ) + ) + + result = self.memory_retriever.add_documents(documents, current_time=now) + + # After an agent has processed a certain amount of memories (as measured by + # aggregate importance), it is time to reflect on recent events to add + # more synthesized memories to the agent's memory stream. + if ( + self.reflection_threshold is not None + and self.aggregate_importance > self.reflection_threshold + and not self.reflecting + ): + self.reflecting = True + self.pause_to_reflect(now=now) + # Hack to clear the importance from reflection + self.aggregate_importance = 0.0 + self.reflecting = False + return result + + def add_memory( + self, memory_content: str, now: Optional[datetime] = None + ) -> List[str]: + """Add an observation or memory to the agent's memory.""" + importance_score = self._score_memory_importance(memory_content) + self.aggregate_importance += importance_score + document = Document( + page_content=memory_content, metadata={"importance": importance_score} + ) + result = self.memory_retriever.add_documents([document], current_time=now) + + # After an agent has processed a certain amount of memories (as measured by + # aggregate importance), it is time to reflect on recent events to add + # more synthesized memories to the agent's memory stream. + if ( + self.reflection_threshold is not None + and self.aggregate_importance > self.reflection_threshold + and not self.reflecting + ): + self.reflecting = True + self.pause_to_reflect(now=now) + # Hack to clear the importance from reflection + self.aggregate_importance = 0.0 + self.reflecting = False + return result + + def fetch_memories( + self, observation: str, now: Optional[datetime] = None + ) -> List[Document]: + """Fetch related memories.""" + if now is not None: + with mock_now(now): + return self.memory_retriever.get_relevant_documents(observation) + else: + return self.memory_retriever.get_relevant_documents(observation) + + def format_memories_detail(self, relevant_memories: List[Document]) -> str: + content = [] + for mem in relevant_memories: + content.append(self._format_memory_detail(mem, prefix="- ")) + return "\n".join([f"{mem}" for mem in content]) + + def _format_memory_detail(self, memory: Document, prefix: str = "") -> str: + created_time = memory.metadata["created_at"].strftime("%B %d, %Y, %I:%M %p") + return f"{prefix}[{created_time}] {memory.page_content.strip()}" + + def format_memories_simple(self, relevant_memories: List[Document]) -> str: + return "; ".join([f"{mem.page_content}" for mem in relevant_memories]) + + def _get_memories_until_limit(self, consumed_tokens: int) -> str: + """Reduce the number of tokens in the documents.""" + result = [] + for doc in self.memory_retriever.memory_stream[::-1]: + if consumed_tokens >= self.max_tokens_limit: + break + consumed_tokens += self.llm.get_num_tokens(doc.page_content) + if consumed_tokens < self.max_tokens_limit: + result.append(doc) + return self.format_memories_simple(result) + + @property + def memory_variables(self) -> List[str]: + """Input keys this memory class will load dynamically.""" + return [] + + def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, str]: + """Return key-value pairs given the text input to the chain.""" + queries = inputs.get(self.queries_key) + now = inputs.get(self.now_key) + if queries is not None: + relevant_memories = [ + mem for query in queries for mem in self.fetch_memories(query, now=now) + ] + return { + self.relevant_memories_key: self.format_memories_detail( + relevant_memories + ), + self.relevant_memories_simple_key: self.format_memories_simple( + relevant_memories + ), + } + + most_recent_memories_token = inputs.get(self.most_recent_memories_token_key) + if most_recent_memories_token is not None: + return { + self.most_recent_memories_key: self._get_memories_until_limit( + most_recent_memories_token + ) + } + return {} + + def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, Any]) -> None: + """Save the context of this model run to memory.""" + # TODO: fix the save memory key + mem = outputs.get(self.add_memory_key) + now = outputs.get(self.now_key) + if mem: + self.add_memory(mem, now=now) + + def clear(self) -> None: + """Clear memory contents.""" + # TODO + + + +####################### MAIN CLASS + + +class WorkerSimsAgent(BaseModel): + """A character with memory and innate characteristics.""" + + name: str + """The character's name.""" + + age: Optional[int] = None + """The optional age of the character.""" + traits: str = "N/A" + """Permanent traits to ascribe to the character.""" + status: str + """The traits of the character you wish not to change.""" + memory: WorkerSims + """The memory object that combines relevance, recency, and 'importance'.""" + llm: BaseLanguageModel + """The underlying language model.""" + verbose: bool = False + summary: str = "" #: :meta private: + """Stateful self-summary generated via reflection on the character's memory.""" + + summary_refresh_seconds: int = 3600 #: :meta private: + """How frequently to re-generate the summary.""" + + last_refreshed: datetime = Field(default_factory=datetime.now) # : :meta private: + """The last time the character's summary was regenerated.""" + + daily_summaries: List[str] = Field(default_factory=list) # : :meta private: + """Summary of the events in the plan that the agent took.""" + + class Config: + """Configuration for this pydantic object.""" + + arbitrary_types_allowed = True + + # LLM-related methods + @staticmethod + def _parse_list(text: str) -> List[str]: + """Parse a newline-separated string into a list of strings.""" + lines = re.split(r"\n", text.strip()) + return [re.sub(r"^\s*\d+\.\s*", "", line).strip() for line in lines] + + def chain(self, prompt: PromptTemplate) -> LLMChain: + return LLMChain( + llm=self.llm, prompt=prompt, verbose=self.verbose, memory=self.memory + ) + + def _get_entity_from_observation(self, observation: str) -> str: + prompt = PromptTemplate.from_template( + "What is the observed entity in the following observation? {observation}" + + "\nEntity=" + ) + return self.chain(prompt).run(observation=observation).strip() + + def _get_entity_action(self, observation: str, entity_name: str) -> str: + prompt = PromptTemplate.from_template( + "What is the {entity} doing in the following observation? {observation}" + + "\nThe {entity} is" + ) + return ( + self.chain(prompt).run(entity=entity_name, observation=observation).strip() + ) + + def summarize_related_memories(self, observation: str) -> str: + """Summarize memories that are most relevant to an observation.""" + prompt = PromptTemplate.from_template( + """ +{q1}? +Context from memory: +{relevant_memories} +Relevant context: +""" + ) + entity_name = self._get_entity_from_observation(observation) + entity_action = self._get_entity_action(observation, entity_name) + q1 = f"What is the relationship between {self.name} and {entity_name}" + q2 = f"{entity_name} is {entity_action}" + return self.chain(prompt=prompt).run(q1=q1, queries=[q1, q2]).strip() + + def _generate_reaction( + self, observation: str, suffix: str, now: Optional[datetime] = None + ) -> str: + """React to a given observation or dialogue act.""" + prompt = PromptTemplate.from_template( + "{agent_summary_description}" + + "\nIt is {current_time}." + + "\n{agent_name}'s status: {agent_status}" + + "\nSummary of relevant context from {agent_name}'s memory:" + + "\n{relevant_memories}" + + "\nMost recent observations: {most_recent_memories}" + + "\nObservation: {observation}" + + "\n\n" + + suffix + ) + agent_summary_description = self.get_summary(now=now) + relevant_memories_str = self.summarize_related_memories(observation) + current_time_str = ( + datetime.now().strftime("%B %d, %Y, %I:%M %p") + if now is None + else now.strftime("%B %d, %Y, %I:%M %p") + ) + kwargs: Dict[str, Any] = dict( + agent_summary_description=agent_summary_description, + current_time=current_time_str, + relevant_memories=relevant_memories_str, + agent_name=self.name, + observation=observation, + agent_status=self.status, + ) + consumed_tokens = self.llm.get_num_tokens( + prompt.format(most_recent_memories="", **kwargs) + ) + kwargs[self.memory.most_recent_memories_token_key] = consumed_tokens + return self.chain(prompt=prompt).run(**kwargs).strip() + + def _clean_response(self, text: str) -> str: + return re.sub(f"^{self.name} ", "", text.strip()).strip() + + def generate_reaction( + self, observation: str, now: Optional[datetime] = None + ) -> Tuple[bool, str]: + """React to a given observation.""" + call_to_action_template = ( + "Should {agent_name} react to the observation, and if so," + + " what would be an appropriate reaction? Respond in one line." + + ' If the action is to engage in dialogue, write:\nSAY: "what to say"' + + "\notherwise, write:\nREACT: {agent_name}'s reaction (if anything)." + + "\nEither do nothing, react, or say something but not both.\n\n" + ) + full_result = self._generate_reaction( + observation, call_to_action_template, now=now + ) + result = full_result.strip().split("\n")[0] + # AAA + self.memory.save_context( + {}, + { + self.memory.add_memory_key: f"{self.name} observed " + f"{observation} and reacted by {result}", + self.memory.now_key: now, + }, + ) + if "REACT:" in result: + reaction = self._clean_response(result.split("REACT:")[-1]) + return False, f"{self.name} {reaction}" + if "SAY:" in result: + said_value = self._clean_response(result.split("SAY:")[-1]) + return True, f"{self.name} said {said_value}" + else: + return False, result + + def generate_dialogue_response( + self, observation: str, now: Optional[datetime] = None + ) -> Tuple[bool, str]: + """React to a given observation.""" + call_to_action_template = ( + "What would {agent_name} say? To end the conversation, write:" + ' GOODBYE: "what to say". Otherwise to continue the conversation,' + ' write: SAY: "what to say next"\n\n' + ) + full_result = self._generate_reaction( + observation, call_to_action_template, now=now + ) + result = full_result.strip().split("\n")[0] + if "GOODBYE:" in result: + farewell = self._clean_response(result.split("GOODBYE:")[-1]) + self.memory.save_context( + {}, + { + self.memory.add_memory_key: f"{self.name} observed " + f"{observation} and said {farewell}", + self.memory.now_key: now, + }, + ) + return False, f"{self.name} said {farewell}" + if "SAY:" in result: + response_text = self._clean_response(result.split("SAY:")[-1]) + self.memory.save_context( + {}, + { + self.memory.add_memory_key: f"{self.name} observed " + f"{observation} and said {response_text}", + self.memory.now_key: now, + }, + ) + return True, f"{self.name} said {response_text}" + else: + return False, result + + ###################################################### + # Agent stateful' summary methods. # + # Each dialog or response prompt includes a header # + # summarizing the agent's self-description. This is # + # updated periodically through probing its memories # + ###################################################### + def _compute_agent_summary(self) -> str: + """""" + prompt = PromptTemplate.from_template( + "How would you summarize {name}'s core characteristics given the" + + " following statements:\n" + + "{relevant_memories}" + + "Do not embellish." + + "\n\nSummary: " + ) + # The agent seeks to think about their core characteristics. + return ( + self.chain(prompt) + .run(name=self.name, queries=[f"{self.name}'s core characteristics"]) + .strip() + ) + + def get_summary( + self, force_refresh: bool = False, now: Optional[datetime] = None + ) -> str: + """Return a descriptive summary of the agent.""" + current_time = datetime.now() if now is None else now + since_refresh = (current_time - self.last_refreshed).seconds + if ( + not self.summary + or since_refresh >= self.summary_refresh_seconds + or force_refresh + ): + self.summary = self._compute_agent_summary() + self.last_refreshed = current_time + age = self.age if self.age is not None else "N/A" + return ( + f"Name: {self.name} (age: {age})" + + f"\nInnate traits: {self.traits}" + + f"\n{self.summary}" + ) + + def get_full_header( + self, force_refresh: bool = False, now: Optional[datetime] = None + ) -> str: + """Return a full header of the agent's status, summary, and current time.""" + now = datetime.now() if now is None else now + summary = self.get_summary(force_refresh=force_refresh, now=now) + current_time_str = now.strftime("%B %d, %Y, %I:%M %p") + return ( + f"{summary}\nIt is {current_time_str}.\n{self.name}'s status: {self.status}" + ) \ No newline at end of file diff --git a/swarms/utils/embeddings/__init__.py b/swarms/utils/embeddings/__init__.py new file mode 100644 index 00000000..dd08cd94 --- /dev/null +++ b/swarms/utils/embeddings/__init__.py @@ -0,0 +1 @@ +from swarms.utils.embeddings.base import Embeddings \ No newline at end of file diff --git a/swarms/agents/embeddings/base.py b/swarms/utils/embeddings/base.py similarity index 100% rename from swarms/agents/embeddings/base.py rename to swarms/utils/embeddings/base.py diff --git a/swarms/agents/embeddings/__init__.py b/swarms/utils/schema/__init__.py similarity index 100% rename from swarms/agents/embeddings/__init__.py rename to swarms/utils/schema/__init__.py diff --git a/swarms/utils/schema/base.py b/swarms/utils/schema/base.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/LLM.py b/tests/LLM.py index 57360294..fbfce733 100644 --- a/tests/LLM.py +++ b/tests/LLM.py @@ -3,7 +3,7 @@ import os from unittest.mock import patch, MagicMock from langchain import PromptTemplate, HuggingFaceHub, ChatOpenAI, LLMChain -from swarms.utils.llm import LLM +from swarms.agents.models.llm import LLM class TestLLM(unittest.TestCase): @patch.object(HuggingFaceHub, '__init__', return_value=None)