Skip to content

Commit

Permalink
Add FunctionTool
Browse files Browse the repository at this point in the history
  • Loading branch information
YoanSallami committed Aug 6, 2024
1 parent 006aced commit 22c452d
Show file tree
Hide file tree
Showing 24 changed files with 440 additions and 71 deletions.
55 changes: 36 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

</div>

**Disclaimer:** We are currently refactoring the project for better modularity and better ease of use. For now, only the focal integration if available, the FalkorDB integration will be done at the end of this refactoring. At that time we will accept contributions for the integration of other Cypher-based graph databases. For more information, join the Discord channel.
**Disclaimer:** We are currently refactoring the project for better modularity and better ease of use. For now, only the Local integration if available, the FalkorDB & Kuzu integration will be done at the end of this refactoring. At that time we will accept contributions for the integration of other Cypher-based graph databases. For more information, join the Discord channel.

## Notebooks

Expand All @@ -29,6 +29,7 @@
- [Episodic RAG](notebook/episodic_memory_rag.ipynb)
- [Extracting Knowledge Graphs](notebook/extracting_knowledge_graphs.ipynb)
- [Dynamic Graph Program](notebook/dynamic_graph_program.ipynb)
- [Using External Tools](notebook/using_external_tools.ipynb)

## What is HybridAGI?

Expand Down Expand Up @@ -56,7 +57,7 @@ pip install hybridagi

**No React Agents here**, the only agent system that we provide is our custom **Graph Interpreter Agent** that follow a strict methodology by executing node by node the graph programs it have in memory. Because we control the behavior of the Agent from end-to-end by offloading planning to symbolic components, we can correct/enhance the behavior of the system easely, removing the needs for finetuning but also allowing the system to learn on the fly.

HybridAGI is build upon years of experience in making reliable robotics systems. We have combined our knowledge in Robotics, Symbolic AI, LLMs and Cognitive Sciences into a product for programmers, data-scientists and AI engineers. The long-term memory of our Agent system heavily use graphs to store structured and unstructured knowledge as well as its graph programs.
HybridAGI is build upon years of experience in making reliable Robotics systems. We have combined our knowledge in Robotics, Symbolic AI, LLMs and Cognitive Sciences into a product for programmers, data-scientists and AI engineers. The long-term memory of our Agent system heavily use graphs to store structured and unstructured knowledge as well as its graph programs.

We provide everything for you to build your LLM application with a focus around Cypher Graph databases. We provide also a local database for rapid prototyping before scaling your application with one of our integration.

Expand Down Expand Up @@ -200,36 +201,48 @@ We provide the following list of native tools to R/W into the memory system or m

</div>

<!-- ### Adding more tools
### Adding more tools

You can easely add more tools by using the `FunctionTool` and python functions like nowadays function calling.
You can add more tools by using the `FunctionTool` and python functions like nowadays function calling.

```python
import requests
from hybridagi.modules.agents.tools import FunctionTool

def my_awesome_tool(foo: str):
# The function inputs should be one or multiple strings, you can then convert or process them in your function
# The docstring and input arguments will be used to create automatically a DSPy signature
def get_crypto_price(crypto_name: str):
"""
The instructions to infer the tools parameters, this is going to be used in DSPy Signature
Please only give the name of the crypto to fetch like "bitcoin" or "cardano"
Never explain or apology, only give the crypto name.
"""
print(foo)
# You should return a dictionary (usually containing the tool inputs + observations/outputs)
return {"message": foo}
tool = FunctionTool(
name = "AwesomeTool",
func = my_awesome_tool,
base_url = "https://api.coingecko.com/api/v3/simple/price?"
complete_url = base_url + "ids=" + crypto_name + "&vs_currencies=usd"
response = requests.get(complete_url)
data = response.json()

# The output of the tool should always be a dict
# It usually contains the sanitized input of the tool + the tool result (or observation)
if crypto_name in data:
return {"crypto_name": crypto_name, "result": str(data[crypto_name]["usd"])+" USD"}
else:
return {"crypto_name": crypto_name, "result": "Invalid crypto name"}

my_tool = FunctionTool(
name = "GetCryptoPrice",
func = get_crypto_price,
)
``` -->
```

### Graph Databases Integrations

- Local Graph Memory for rapid prototyping
- Local Graph Memory for rapid prototyping based on [NetworkX](https://networkx.org/)
- [FalkorDB](https://www.falkordb.com/) low latency in-memory hybrid vector/graph database (coming soon)
- [Kuzu](https://kuzudb.com/) A highly scalable, extremely fast, easy-to-use embeddable graph database (coming soon)

### LLM Agent as Graph VS LLM Agent as Graph Interpreter

What makes our approach different from Agent as Graph is the fact that our agent system is not a process represented by a Graph, but an interpreter that can read/write and execute a graph data structure separated from that process. Making possible for the Agent to learn by executing, reading and modifying the graph data (like any other data), in its essence HybridAGI is intended to be a self-programming system centered around the Cypher language. It is a production-ready research project centered around neuro-symbolic programming, program synthesis and symbolic AI.
What makes our approach different from Agent as Graph is the fact that our Agent system is not a process represented by a graph, but an interpreter that can read/write and execute a graph data (the graph programs) structure separated from that process. Making possible for the Agent to learn by executing, reading and modifying the graph programs (like any other data), in its essence HybridAGI is intended to be a self-programming system centered around the Cypher language. It is a production-ready research project centered around neuro-symbolic programming, program synthesis and symbolic AI.

### Differences with LangGraph/LangChain or Llama-Index

Expand Down Expand Up @@ -257,9 +270,13 @@ We're not based in Silicon Valley or part of a big company; we're a small, dedic

Our mission extends beyond AI safety and performance; it's about shaping the world we want to live in. Even if programming becomes obsolete in 5 or 10 years, replaced by some magical prompt, we believe that traditional prompts are insufficient for preserving jobs. They're too simplistic and *fail to accurately convey intentions*.

In contrast, programming each reasoning step demands expert knowledge in prompt engineering and programming. Surprisingly, it's enjoyable and not that difficult for programmers, you'll gain insight into how AI truly operates by controlling it, beeing able to enhance the sequence of action and decision. Natural language combined with algorithms opens up endless possibilities. We can't envision a world without it.
In contrast, programming each reasoning step demands expert knowledge in prompt engineering and programming. Surprisingly, it's enjoyable and not that difficult for programmers, you'll gain insight into how AI truly operates by controlling it, being able to enhance the sequence of action and decision. Natural language combined with algorithms opens up endless possibilities. We can't envision a world without it.

## Commercial Usage

Our software is released under GNU GPL license to protect ourselves and the contributions of the community.
The logic of your application being separated (the graph programs) there is no IP problems for you to use HybridAGI. Moreover when used in production, you surely want to make a FastAPI server to request your agent and separate the backend and frontend of your app (like a website), so the GPL license doesn't contaminate the other pieces of your software.
The logic of your application being separated (the graph programs) there is no IP problems for you to use HybridAGI. Moreover when used in production, you surely want to make a FastAPI server to request your agent and separate the backend and frontend of your app (like a website), so the GPL license doesn't contaminate the other pieces of your software.

## Star History

[![Star History Chart](https://api.star-history.com/svg?repos=SynaLinks/HybridAGI&type=Date)](https://star-history.com/#SynaLinks/HybridAGI&Date)
23 changes: 14 additions & 9 deletions hybridagi/modules/agents/graph_interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def __init__(
num_history: int = 5,
max_iters: int = 20,
verbose: bool = True,
debug: bool = False,
):
"""
Initializes the Graph Interpreter Agent.
Expand All @@ -96,6 +97,7 @@ def __init__(
self.decision_parser = DecisionOutputParser()
self.prediction_parser = PredictionOutputParser()
self.verbose = verbose
self.debug = debug
self.previous_agent_step = None
if self.trace_memory is not None:
if self.embeddings is None:
Expand Down Expand Up @@ -361,15 +363,18 @@ def forward(self, query_or_query_with_session: Union[Query, QueryWithSession]) -
"""
self.start(query_or_query_with_session)
for i in range(self.max_iters):
# try:
self.run_step()
# except Exception as e:
# return AgentOutput(
# finish_reason = FinishReason.Error,
# final_answer = "Error occured: "+str(e),
# program_trace = self.agent_state.program_trace,
# session = self.agent_state.session,
# )
if self.debug is False:
try:
self.run_step()
except Exception as e:
return AgentOutput(
finish_reason = FinishReason.Error,
final_answer = "Error occured: "+str(e),
program_trace = self.agent_state.program_trace,
session = self.agent_state.session,
)
else:
self.run_step()
if self.finished():
return AgentOutput(
finish_reason = FinishReason.Finished,
Expand Down
4 changes: 2 additions & 2 deletions hybridagi/modules/agents/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from .call_graph_program import CallGraphProgramTool

# Generic function calling tool
# from.function_tool import FunctionTool
from .function_tool import FunctionTool

__all__ = [
SpeakTool,
Expand All @@ -36,5 +36,5 @@
UpdateObjectiveTool,
CallGraphProgramTool,

# FunctionTool,
FunctionTool,
]
2 changes: 2 additions & 0 deletions hybridagi/modules/agents/tools/ask_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ def simulate_ask_user(self, question: str):
return pred.answer

def forward(self, tool_input: ToolInput) -> AskUserOutput:
if not isinstance(tool_input, ToolInput):
raise ValueError(f"{type(self).__name__} input must be a ToolInput")
if not tool_input.disable_inference:
with dspy.context(lm=self.lm if self.lm is not None else dspy.settings.lm):
pred = self.predict(
Expand Down
2 changes: 2 additions & 0 deletions hybridagi/modules/agents/tools/call_graph_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def call_program(self, program_name: str) -> str:
return "Successfully called"

def forward(self, tool_input: ToolInput) -> CallGraphProgramOutput:
if not isinstance(tool_input, ToolInput):
raise ValueError(f"{type(self).__name__} input must be a ToolInput")
if not tool_input.disable_inference:
with dspy.context(lm=self.lm if self.lm is not None else dspy.settings.lm):
pred = self.predict(
Expand Down
2 changes: 2 additions & 0 deletions hybridagi/modules/agents/tools/chain_of_thought.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ def __init__(
self.prediction_parser = PredictionOutputParser()

def forward(self, tool_input: ToolInput) -> ChainOfThoughtOutput:
if not isinstance(tool_input, ToolInput):
raise ValueError(f"{type(self).__name__} input must be a ToolInput")
if not tool_input.disable_inference:
with dspy.context(lm=self.lm if self.lm is not None else dspy.settings.lm):
pred = self.predict(
Expand Down
2 changes: 2 additions & 0 deletions hybridagi/modules/agents/tools/document_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ def document_search(self, query: str):
return self.retriever(retriver_input)

def forward(self, tool_input: ToolInput) -> QueryWithDocuments:
if not isinstance(tool_input, ToolInput):
raise ValueError(f"{type(self).__name__} input must be a ToolInput")
if not tool_input.disable_inference:
with dspy.context(lm=self.lm if self.lm is not None else dspy.settings.lm):
pred = self.predict(
Expand Down
2 changes: 2 additions & 0 deletions hybridagi/modules/agents/tools/entity_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ def entity_search(self, query: str):
return self.retriever(retriver_input)

def forward(self, tool_input: ToolInput) -> QueryWithEntities:
if not isinstance(tool_input, ToolInput):
raise ValueError(f"{type(self).__name__} input must be a ToolInput")
if not tool_input.disable_inference:
with dspy.context(lm=self.lm if self.lm is not None else dspy.settings.lm):
pred = self.predict(
Expand Down
2 changes: 2 additions & 0 deletions hybridagi/modules/agents/tools/fact_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ def fact_search(self, query: str):
return self.retriever(retriver_input)

def forward(self, tool_input: ToolInput) -> QueryWithFacts:
if not isinstance(tool_input, ToolInput):
raise ValueError(f"{type(self).__name__} input must be a ToolInput")
if not tool_input.disable_inference:
with dspy.context(lm=self.lm if self.lm is not None else dspy.settings.lm):
pred = self.predict(
Expand Down
75 changes: 74 additions & 1 deletion hybridagi/modules/agents/tools/function_tool.py
Original file line number Diff line number Diff line change
@@ -1 +1,74 @@
#TODO
import dspy
import inspect
from hybridagi.core.datatypes import ToolInput
from typing import Optional, Callable, Any
from .tool import Tool

class FunctionToolOutput(dspy.Prediction):

def __init__(self, **kwargs):
dspy.Prediction.__init__(self, **kwargs)

def to_dict(self):
return dict(self)

class FunctionTool(Tool):

def __init__(
self,
name: str,
func: Callable,
lm: Optional[dspy.LM] = None,
):
super().__init__(lm = lm, name = name)
signature_dict = {}
signature_dict["objective"] = dspy.InputField(
prefix = "Objective:",
desc = "The long-term objective (what you are doing)",
)
signature_dict["context"] = dspy.InputField(
prefix = "Context:",
desc = "The previous actions (what you have done)",
)
signature_dict["purpose"] = dspy.InputField(
prefix = "Purpose:",
desc = "The purpose of the action (what you have to do now)",
)
signature_dict["prompt"] = dspy.InputField(
prefix = "Prompt:",
desc = "The action specific instructions (How to do it)",
)

func_signature = inspect.signature(func)

for param in func_signature.parameters:
if not isinstance(param, str):
raise ValueError(f"{type(self).__name__} function calling only support string inputs")
signature_dict[param] = dspy.OutputField(prefix=param.title()+":")

docstring = func.__doc__
if docstring is not None:
instr = docstring.strip()
self.predict = dspy.Predict(dspy.Signature(signature_dict, instr))
else:
self.predict = dspy.Predict(dspy.Signature(signature_dict))
self.func = func

def forward(self, tool_input: ToolInput) -> FunctionToolOutput:
if not tool_input.disable_inference:
with dspy.context(lm=self.lm if self.lm is not None else dspy.settings.lm):
pred = self.predict(
objective = tool_input.objective,
context = tool_input.context,
purpose = tool_input.purpose,
prompt = tool_input.prompt,
)
args = {}
for param in inspect.signature(self.func).parameters:
args[param] = pred[param]
result = self.func(**args)
return FunctionToolOutput(
**result
)
else:
raise NotImplementedError(f"{type(self).__name__} doesn't support disabling inference")
2 changes: 2 additions & 0 deletions hybridagi/modules/agents/tools/graph_program_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ def program_search(self, query: str):
return self.retriever(retriver_input)

def forward(self, tool_input: ToolInput) -> GraphProgramList:
if not isinstance(tool_input, ToolInput):
raise ValueError(f"{type(self).__name__} input must be a ToolInput")
if not tool_input.disable_inference:
with dspy.context(lm=self.lm if self.lm is not None else dspy.settings.lm):
pred = self.predict(
Expand Down
2 changes: 2 additions & 0 deletions hybridagi/modules/agents/tools/past_action_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ def action_search(self, query: str):
return self.retriever(retriver_input)

def forward(self, tool_input: ToolInput) -> AgentStepList:
if not isinstance(tool_input, ToolInput):
raise ValueError(f"{type(self).__name__} input must be a ToolInput")
if not tool_input.disable_inference:
with dspy.context(lm=self.lm if self.lm is not None else dspy.settings.lm):
pred = self.predict(
Expand Down
2 changes: 2 additions & 0 deletions hybridagi/modules/agents/tools/predict.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ def __init__(
self.prediction_parser = PredictionOutputParser()

def forward(self, tool_input: ToolInput) -> PredictOutput:
if not isinstance(tool_input, ToolInput):
raise ValueError(f"{type(self).__name__} input must be a ToolInput")
if not tool_input.disable_inference:
with dspy.context(lm=self.lm if self.lm is not None else dspy.settings.lm):
pred = self.predict(
Expand Down
2 changes: 2 additions & 0 deletions hybridagi/modules/agents/tools/read_graph_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def read_graph_program(self, name: str):
return self.program_memory.get(name).progs[0]

def forward(self, tool_input: ToolInput) -> PredictOutput:
if not isinstance(tool_input, ToolInput):
raise ValueError(f"{type(self).__name__} input must be a ToolInput")
if not tool_input.disable_inference:
with dspy.context(lm=self.lm if self.lm is not None else dspy.settings.lm):
pred = self.predict(
Expand Down
2 changes: 2 additions & 0 deletions hybridagi/modules/agents/tools/speak.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ def speak(self, message: str):
)

def forward(self, tool_input: ToolInput) -> SpeakOutput:
if not isinstance(tool_input, ToolInput):
raise ValueError(f"{type(self).__name__} input must be a ToolInput")
if not tool_input.disable_inference:
with dspy.context(lm=self.lm if self.lm is not None else dspy.settings.lm):
pred = self.predict(
Expand Down
9 changes: 7 additions & 2 deletions hybridagi/modules/agents/tools/tool.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from abc import ABC, abstractmethod
import dspy
import copy
from hybridagi.core.datatypes import ToolInput
from typing import Optional, Union, Callable, Dict, Any
from dspy.signatures.signature import ensure_signature

Expand All @@ -15,5 +16,9 @@ def __init__(
self.lm = lm

@abstractmethod
def forward(self, **kwargs) -> dspy.Prediction:
pass
def forward(self, tool_input: ToolInput) -> dspy.Prediction:
if not isinstance(tool_input, ToolInput):
raise ValueError(f"{type(self).__name__} input must be a ToolInput")
raise NotImplementedError(
f"Tool {type(self).__name__} is missing the required 'forward' method."
)
2 changes: 2 additions & 0 deletions hybridagi/modules/agents/tools/update_objective.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ def __init__(
self.agent_state = agent_state

def forward(self, tool_input: ToolInput) -> UpdateObjectiveOutput:
if not isinstance(tool_input, ToolInput):
raise ValueError(f"{type(self).__name__} input must be a ToolInput")
if not tool_input.disable_inference:
with dspy.context(lm=self.lm if self.lm is not None else dspy.settings.lm):
pred = self.predict(
Expand Down
1 change: 0 additions & 1 deletion hybridagi/modules/extractors/llm_facts_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ class FactsExtractorSignature(dspy.Signature):
"""
document: str = dspy.InputField(desc="The input document")
triplets: str = dspy.OutputField(desc="The comma separated triplets extracted from the document")


class LLMFactsExtractor(FactExtractor):

Expand Down
Loading

0 comments on commit 22c452d

Please sign in to comment.