Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ cd group-genie
Create a virtual environment and install dependencies:

```bash
uv sync
uv sync --all-extras
```

Activate the virtual environment:
Expand Down
5 changes: 5 additions & 0 deletions docs/api/provider/openai.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
::: group_genie.agent.provider.openai.DefaultAgent
options:
members:
- run
- mcp
2 changes: 1 addition & 1 deletion docs/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Integrating Group Genie into a chat server involves connecting two key component

## Key Integration Points

A chat server usually provides two integration points. The following are specific to the [group-terminal](https://github.com/gradion-ai/group-terminal) chat server:
A chat server usually provides two integration points. The following are specific to the [group-terminal](https://gradion-ai.github.io/group-terminal/) chat server:

1. **Message Handler**: A callback that receives incoming messages from chat clients
- Signature: `async def handler(content: str, sender: str)`
Expand Down
28 changes: 17 additions & 11 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ Each group chat participant owns both a group reasoner instance and a system age

We implement [`SecretsProvider`][group_genie.secrets.SecretsProvider], an interface designed to retrieve user-specific credentials:

```python title="examples/factory/provider.py"
--8<-- "examples/factory/provider.py:imports"
```python title="examples/factory/secrets.py"
--8<-- "examples/factory/secrets.py:imports"

--8<-- "examples/factory/provider.py:secrets-provider"
--8<-- "examples/factory/secrets.py:secrets-provider"
```

In this development example, we just return the same set of environment variables for all users. In production, you would implement per-user credential storage and retrieval.
Expand All @@ -61,11 +61,11 @@ In this development example, we just return the same set of environment variable

We use the `get_group_reasoner_factory` helper to obtain a [`GroupReasonerFactory`][group_genie.reasoner.factory.GroupReasonerFactory] for creating user-specific reasoner instances:

```python title="examples/factory/reasoner.py"
--8<-- "examples/factory/reasoner.py:imports"
```python title="examples/factory/pydantic_ai/reasoner_factory.py"
--8<-- "examples/factory/pydantic_ai/reasoner_factory.py:imports"

--8<-- "examples/factory/reasoner.py:create-group-reasoner"
--8<-- "examples/factory/reasoner.py:group-reasoner-factory"
--8<-- "examples/factory/pydantic_ai/reasoner_factory.py:create-group-reasoner"
--8<-- "examples/factory/pydantic_ai/reasoner_factory.py:group-reasoner-factory"
```

The `create_group_reasoner` function receives a system prompt template, secrets, and the owner's username and returns a configured [`GroupReasoner`][group_genie.reasoner.base.GroupReasoner]. It:
Expand All @@ -76,13 +76,19 @@ The `create_group_reasoner` function receives a system prompt template, secrets,

### Agent Factory

!!! hint "Agent frameworks"

Group Genie supports multiple agent frameworks through the [`Agent`][group_genie.agent.base.Agent] interface. The following example factory is defined in [`pydantic_ai/agent_factory_1.py`](https://github.com/gradion-ai/group-genie/blob/main/examples/factory/pydantic_ai/agent_factory_1.py). It uses [Pydantic AI](https://ai.pydantic.dev/) through a [default implementation][group_genie.agent.provider.pydantic_ai.agent.default.DefaultAgent] of the [`Agent`][group_genie.agent.base.Agent] interface.

An equivalent example using the [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) with another [default implementation][group_genie.agent.provider.openai.agent.DefaultAgent] of the [`Agent`][group_genie.agent.base.Agent] interface is defined in [`openai/agent_factory_1.py`](https://github.com/gradion-ai/group-genie/blob/main/examples/factory/openai/agent_factory_1.py). You can also integrate any other agent framework or API by implementing the [`Agent`][group_genie.agent.base.Agent] interface directly.

We use the `get_agent_factory` helper to obtain an [`AgentFactory`][group_genie.agent.factory.AgentFactory] for creating user-specific system agent instances:

```python title="examples/factory/agent_1.py"
--8<-- "examples/factory/agent_1.py:imports"
```python title="examples/factory/pydantic_ai/agent_factory_1.py"
--8<-- "examples/factory/pydantic_ai/agent_factory_1.py:imports"

--8<-- "examples/factory/agent_1.py:create-system-agent"
--8<-- "examples/factory/agent_1.py:agent-factory"
--8<-- "examples/factory/pydantic_ai/agent_factory_1.py:create-system-agent"
--8<-- "examples/factory/pydantic_ai/agent_factory_1.py:agent-factory"
```

The `create_system_agent` function receives the owner's secrets and returns a configured [`Agent`][group_genie.agent.base.Agent]. It:
Expand Down
41 changes: 41 additions & 0 deletions examples/factory/openai/agent_factory_1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from agents import ModelSettings, OpenAIResponsesModel
from agents.mcp import MCPServerStdio
from openai import AsyncOpenAI

from group_genie.agent import Agent, AgentFactory
from group_genie.agent.provider.openai import DefaultAgent
from group_genie.secrets import SecretsProvider


def create_system_agent(secrets: dict[str, str]) -> Agent:
brave_mcp_server = MCPServerStdio(
name="Brave Search",
params={
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-brave-search"],
"env": {
"BRAVE_API_KEY": secrets.get("BRAVE_API_KEY", ""),
},
},
)

return DefaultAgent(
system_prompt=(
"You are a helpful assistant. "
"Always search the web for checking facts. "
"Provide short, concise answers."
),
model=OpenAIResponsesModel(
model="gpt-4.1",
openai_client=AsyncOpenAI(api_key=secrets.get("OPENAI_API_KEY", "")),
),
model_settings=ModelSettings(),
mcp_servers=[brave_mcp_server],
)


def get_agent_factory(secrets_provider: SecretsProvider | None = None):
return AgentFactory(
system_agent_factory=create_system_agent,
secrets_provider=secrets_provider,
)
97 changes: 97 additions & 0 deletions examples/factory/openai/agent_factory_2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from agents import ModelSettings, OpenAIResponsesModel, function_tool
from agents.mcp import MCPServerStdio
from openai import AsyncOpenAI

from group_genie.agent import Agent, AgentFactory, AgentInfo, AsyncTool
from group_genie.agent.provider.openai import DefaultAgent
from group_genie.secrets import SecretsProvider


def create_search_agent(secrets: dict[str, str]) -> Agent:
brave_mcp_server = MCPServerStdio(
name="Brave Search",
params={
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-brave-search"],
"env": {
"BRAVE_API_KEY": secrets.get("BRAVE_API_KEY", ""),
},
},
)

return DefaultAgent(
system_prompt="You are a search specialist. Use web search to find accurate, up-to-date information. Provide concise answers.",
model=OpenAIResponsesModel(
model="gpt-4.1",
openai_client=AsyncOpenAI(api_key=secrets.get("OPENAI_API_KEY", "")),
),
model_settings=ModelSettings(),
mcp_servers=[brave_mcp_server],
)


def create_math_agent(secrets: dict[str, str]) -> Agent:
ipybox_mcp_server = MCPServerStdio(
name="IPyBox Python Executor",
params={
"command": "uvx",
"args": ["ipybox", "mcp"],
},
)

return DefaultAgent(
system_prompt="You are a computational mathematician. For every math problem, write and execute Python code to calculate the answer.",
model=OpenAIResponsesModel(
model="gpt-4.1",
openai_client=AsyncOpenAI(api_key=secrets.get("OPENAI_API_KEY", "")),
),
model_settings=ModelSettings(),
mcp_servers=[ipybox_mcp_server],
)


def create_system_agent(
secrets: dict[str, str],
extra_tools: dict[str, AsyncTool],
agent_infos: list[AgentInfo],
) -> Agent:
from examples.prompts.coordinator.prompt import system_prompt

tools = [function_tool(extra_tools["run_subagent"])]
if tool := extra_tools.get("get_group_chat_messages"):
tools.append(function_tool(tool))

return DefaultAgent(
system_prompt=system_prompt(agent_infos),
model=OpenAIResponsesModel(
model="gpt-4.1",
openai_client=AsyncOpenAI(api_key=secrets.get("OPENAI_API_KEY", "")),
),
model_settings=ModelSettings(),
tools=tools,
)


def get_agent_factory(secrets_provider: SecretsProvider | None = None):
registry = AgentFactory(
system_agent_factory=create_system_agent,
secrets_provider=secrets_provider,
)

registry.add_agent_factory_fn(
factory_fn=create_search_agent,
info=AgentInfo(
name="search",
description="Searches the web for current information. Use for real-time facts, recent events, or up-to-date data.",
),
)

registry.add_agent_factory_fn(
factory_fn=create_math_agent,
info=AgentInfo(
name="math",
description="Solves math problems using Python code execution. Use for calculations or numerical analysis.",
),
)

return registry
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def create_system_agent(
) -> Agent:
from examples.prompts.coordinator.prompt import system_prompt

tools = [extra_tools["run_subagent"]]
tools: list[AsyncTool] = [extra_tools["run_subagent"]]
if tool := extra_tools.get("get_group_chat_messages"):
tools.append(tool)

Expand Down
6 changes: 2 additions & 4 deletions examples/factory/provider.py → examples/factory/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@
class EnvironmentSecretsProvider(SecretsProvider):
def get_secrets(self, username: str) -> dict[str, str] | None:
# For development: use environment variables for all users
return {
"GOOGLE_API_KEY": os.getenv("GOOGLE_API_KEY", ""),
"BRAVE_API_KEY": os.getenv("BRAVE_API_KEY", ""),
}
var_names = ["OPENAI_API_KEY", "GOOGLE_API_KEY", "BRAVE_API_KEY"]
return {var_name: os.getenv(var_name, "") for var_name in var_names}


# --8<-- [end:secrets-provider]
8 changes: 4 additions & 4 deletions examples/guide/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from dotenv import load_dotenv
from group_terminal.server import ChatServer

from examples.factory.agent_2 import get_agent_factory
from examples.factory.provider import EnvironmentSecretsProvider
from examples.factory.reasoner import get_group_reasoner_factory
from examples.factory.pydantic_ai.agent_factory_2 import get_agent_factory
from examples.factory.pydantic_ai.reasoner_factory import get_group_reasoner_factory
from examples.factory.secrets import EnvironmentSecretsProvider
from group_genie.agent import AgentFactory, Approval, Decision
from group_genie.datastore import DataStore
from group_genie.logging import configure_logging
Expand All @@ -31,7 +31,7 @@ def __init__(
port: int = 8723,
):
self._session_id = session_id or identifier()
self._data_store = DataStore(root_path=Path(".data", "chat", self._session_id))
self._data_store = DataStore(root_path=Path(".data", "chat"))
self._session = GroupSession(
id=self._session_id,
group_reasoner_factory=group_reasoner_factory,
Expand Down
59 changes: 59 additions & 0 deletions examples/guide/hierarchy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import asyncio
import logging
from pathlib import Path

from examples.factory.pydantic_ai.agent_factory_1 import get_agent_factory
from examples.factory.pydantic_ai.reasoner_factory import get_group_reasoner_factory
from examples.factory.secrets import EnvironmentSecretsProvider
from examples.utils import complete_execution
from group_genie.datastore import DataStore
from group_genie.logging import configure_logging
from group_genie.message import Message
from group_genie.session import GroupSession
from group_genie.utils import identifier

logger = logging.getLogger(__name__)


async def main():
secrets_provider = EnvironmentSecretsProvider()
session_id = identifier()
session = GroupSession(
id=session_id,
group_reasoner_factory=get_group_reasoner_factory(
secrets_provider=secrets_provider,
template_name="general_assist",
),
agent_factory=get_agent_factory(secrets_provider=secrets_provider),
data_store=DataStore(root_path=Path(".data", "hierarchy")),
)

message = Message(
content="what is the current population of Graz, Austria raised to the power of 0.13?",
sender="user1",
)

# Uses the system agent as coordinator for subagents "search" and "math".
execution = session.handle(message)
await complete_execution(execution)
# Output should be something like this:
# 2025-11-10 07:16:25,116 DEBUG examples.utils: Decision.DELEGATE
# 2025-11-10 07:16:26,959 DEBUG examples.utils: [sender="system"] run_subagent(query='current population of Graz Austria', subagent_name='search', subagent_instance=None, attachments=[])
# Note: the search subagent uses the builtin web search tool provided by the Gemini API, hence no tool call is logged.
# 2025-11-10 07:16:31,771 DEBUG examples.utils: [sender="system"] run_subagent(query='306068^0.13', subagent_name='math', subagent_instance=None, attachments=[])
# 2025-11-10 07:16:32,587 DEBUG examples.utils: [sender="math:d330a02c"] execute_ipython_cell(code='print(306068**0.13)')
# 2025-11-10 07:16:38,324 DEBUG examples.utils: Message(content='The current population of Graz, Austria (projected to be 306,068 as of January 1, 2025), raised to the power of 0.13 is approximately 5.166.', sender='system', receiver='user1', threads=[], attachments=[], request_id=None)

session.stop()
await session.join()


if __name__ == "__main__":
with configure_logging(
levels={
"examples": logging.DEBUG,
"group_sense": logging.INFO,
"group_genie": logging.INFO,
}
):
asyncio.run(main())
8 changes: 4 additions & 4 deletions examples/guide/tutorial.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from pathlib import Path
from uuid import uuid4

from examples.factory.agent_1 import get_agent_factory
from examples.factory.provider import EnvironmentSecretsProvider
from examples.factory.reasoner import get_group_reasoner_factory
from examples.factory.pydantic_ai.agent_factory_1 import get_agent_factory
from examples.factory.pydantic_ai.reasoner_factory import get_group_reasoner_factory
from examples.factory.secrets import EnvironmentSecretsProvider
from group_genie.agent import Approval, Decision
from group_genie.datastore import DataStore
from group_genie.message import Message
Expand All @@ -28,7 +28,7 @@ async def main():
template_name="fact_check",
),
agent_factory=get_agent_factory(secrets_provider=secrets_provider),
data_store=DataStore(root_path=Path(".data", "quickstart", session_id)),
data_store=DataStore(root_path=Path(".data", "tutorial")),
)

chat = [ # example group chat messages
Expand Down
16 changes: 9 additions & 7 deletions examples/misc/attach.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
import logging
from pathlib import Path

from examples.factory.agent_2 import get_agent_factory
from examples.factory.reasoner import get_group_reasoner_factory
from examples.factory.pydantic_ai.agent_factory_1 import get_agent_factory
from examples.factory.pydantic_ai.reasoner_factory import get_group_reasoner_factory
from examples.factory.secrets import EnvironmentSecretsProvider
from examples.utils import complete_execution
from group_genie.datastore import DataStore
from group_genie.logging import configure_logging
Expand All @@ -15,12 +16,13 @@


async def main():
secrets_provider = EnvironmentSecretsProvider()
session_id = identifier()
session = GroupSession(
id=session_id,
group_reasoner_factory=get_group_reasoner_factory(),
agent_factory=get_agent_factory(),
data_store=DataStore(root_path=Path(".data", "attach", session_id)),
group_reasoner_factory=get_group_reasoner_factory(secrets_provider=secrets_provider),
agent_factory=get_agent_factory(secrets_provider=secrets_provider),
data_store=DataStore(root_path=Path(".data", "attach")),
)

attachment_1 = Attachment(
Expand All @@ -30,13 +32,13 @@ async def main():
)

message_1 = Message(
content="Look at this nice image!",
content="I'm feeling good!",
sender="user2",
attachments=[attachment_1],
)

message_2 = Message(
content="What is the weather in the city mentioned in the attached image?",
content="What is the weather in the city mentioned in the image?",
sender="user1",
)

Expand Down
Loading