├── .gitignore
├── README.md
├── docs
└── imgs
│ ├── accesskey.png
│ ├── llp.png
│ ├── new_sdk_visual.png
│ └── old_sdk_visual.png
├── examples
├── game
│ ├── test_agent.py
│ └── test_worker.py
└── twitter_agent
│ ├── example-custom.py
│ └── example-twitter.py
├── pyproject.toml
└── src
└── virtuals_sdk
├── __init__.py
├── game
├── README.md
├── __init__.py
├── agent.py
├── custom_types.py
├── utils.py
└── worker.py
└── twitter_agent
├── README.md
├── __init__.py
├── agent.py
├── functions
├── __init__.py
├── discord.py
├── farcaster.py
└── telegram.py
└── sdk.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pth
2 | *.env
3 | *#
4 | *~
5 | *.pyc
6 | *__pycache__
7 | *.json
8 |
9 | *.DS_Store
10 | dist/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Virtuals Python SDK Library [WARNING: MIGRATING - WILL BE DEPRECATED]
2 | THIS REPOSITORY WILL NO LONGER BE MAINTAINED AND HAS BEEN MIGRATED OVER TO https://github.com/game-by-virtuals/game-python/tree/main. Please move over to use latest SDK and features.
3 |
4 | The Virtuals Python SDK is a library that allows you interact with the Virtuals Platform.
5 |
6 | ## About G.A.M.E.
7 | GAME is a modular agentic framework which enables an agent to plan actions and make decisions autonomously based on information provided to it.
8 |
9 | Please refer to our [whitepaper](https://whitepaper.virtuals.io/developer-documents/game-framework) for more information and resources.
10 |
11 | ## About Virtuals Python SDK
12 | Currently, this SDK allows you to develop your agents powered by the GAME architecture in its most fullest and most flexible form.
13 |
14 | 
15 | The python SDK is made up of 3 main components (Agent, Worker, function), each with configurable arguments.
16 |
17 | Agent (a.k.a. [high level planner](https://whitepaper.virtuals.io/developer-documents/game-framework/game-overview#high-level-planner-hlp-context))
18 | - Takes in a Goal
19 | - Drives the agents behaviour through the high level plan which influences the thinking and creation of tasks that would contribute towards this goal
20 | - Takes in a Description
21 | - Combination of what was previously known as World Info + Agent Description
22 | - This include a description of the "world" the agent lives in, and the personality and background of the agent
23 |
24 | Worker (a.k.a. [low-level planner](https://whitepaper.virtuals.io/developer-documents/game-framework/game-overview#low-level-planner-llp-context))
25 | - Takes in a Description
26 | - Used to control which workers are called by the agent, based on the high-level plan and tasks created to contribute to the goal
27 |
28 | Function
29 | - Takes in a Description
30 | - Used to control which functions are called by the workers, based on each worker's low-level plan
31 | - This can be any python executable
32 |
33 | ## Features
34 | - Develop your own custom agents for any application or platform.
35 | - Ability to control your agents and workers via descriptions (prompts)
36 | - Full control of what the agent sees (state) and can do (actions/functions)
37 | - Ability to fully customise functions. This could include various combinations of programmed logic. For example:
38 | - Calling an API to retrieve data
39 | - Calling an API to retrieve data, followed by custom calculations or data processing logic in python code
40 | - 2 API calls chained together (e.g. calling an API to retrieve web data, and then posting a tweet)
41 |
42 | > ### ℹ️ Changes from older python SDK version (prior to 8 Jan 2025)
43 | >
44 | > - Ability to fully customise functions (previously, each function was a single API call)
45 | > - Ability to control the low-level planner via description prompt (previously, only the high-level planner and functions could be controlled via description prompts)
46 | > - The description defined in the agent is equivalent to what was previously known as world information and agent description
47 |
48 | ## Installation
49 | ```bash
50 | pip install virtuals_sdk
51 | ```
52 |
53 | ## Usage
54 | Please refer to [`test_agent.py`](examples/game/test_agent.py) and [`test_worker.py`](examples/game/test_agent.py) for usage examples.
55 |
56 | ## How to Contribute
57 | Contributions are welcome, especially in the form of new plugins! We are working on creating a plugins repo, but in meantime - please contact us via [Twitter](https://x.com/GAME_Virtuals) or [Telegram](https://t.me/virtuaIs_io).
58 |
59 | ## Documentation
60 | Detailed documentation to better understand the configurable components and the GAME architecture can be found on [here](https://whitepaper.virtuals.io/developer-documents/game-framework).
61 |
62 | ## Useful Resources
63 | - [TypeScript SDK](https://www.npmjs.com/package/@virtuals-protocol/game): The core logic of this SDK mirrors the logic of this python SDK if you prefer to develop your agents in TypeScript.
64 | - [Twitter Agent](./src/virtuals_sdk/twitter_agent/README.md): The Virtuals Platform offers a out-of-the-box hosted agent that can be used to interact with the Twitter/X platform, powered by GAME. This agent comes with existing functions/actions that can be used to interact with the Twitter/X platform and can be immediately hosted/deployed as you configure it. This is similar to configuring your agent in the [Agent Sandbox](https://game-lite.virtuals.io/) on the [Virtuals Platform](https://app.virtuals.io/) but through a developer-friendly SDK interface.
65 |
--------------------------------------------------------------------------------
/docs/imgs/accesskey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Virtual-Protocol/virtuals-python/c9585fac924ccc83e61206728f220b91115aa4d4/docs/imgs/accesskey.png
--------------------------------------------------------------------------------
/docs/imgs/llp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Virtual-Protocol/virtuals-python/c9585fac924ccc83e61206728f220b91115aa4d4/docs/imgs/llp.png
--------------------------------------------------------------------------------
/docs/imgs/new_sdk_visual.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Virtual-Protocol/virtuals-python/c9585fac924ccc83e61206728f220b91115aa4d4/docs/imgs/new_sdk_visual.png
--------------------------------------------------------------------------------
/docs/imgs/old_sdk_visual.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Virtual-Protocol/virtuals-python/c9585fac924ccc83e61206728f220b91115aa4d4/docs/imgs/old_sdk_visual.png
--------------------------------------------------------------------------------
/examples/game/test_agent.py:
--------------------------------------------------------------------------------
1 | from virtuals_sdk.game.agent import Agent, WorkerConfig
2 | from virtuals_sdk.game.custom_types import Function, Argument
3 | from virtuals_sdk.game.custom_types import FunctionResult, FunctionResultStatus
4 | from typing import Tuple
5 |
6 | game_api_key=""
7 |
8 | # the get_worker_state_fn and the get_agent_state_fn can be the same function or diffferent
9 | # each worker can also have a different get_worker_state_fn and maintain their own state
10 | # or they can share the same get_worker_state_fn and maintain the same state
11 |
12 | def get_worker_state_fn(function_result: FunctionResult, current_state: dict) -> dict:
13 | """
14 | This function will get called at every step of the workers execution to form the agent's state.
15 | It will take as input the function result from the previous step.
16 | """
17 | # dict containing info about the function result as implemented in the exectuable
18 | info = function_result.info
19 |
20 | # example of fixed state (function result info is not used to change state) - the first state placed here is the initial state
21 | init_state = {
22 | "objects": [
23 | {"name": "apple", "description": "A red apple", "type": ["item", "food"]},
24 | {"name": "banana", "description": "A yellow banana", "type": ["item", "food"]},
25 | {"name": "orange", "description": "A juicy orange", "type": ["item", "food"]},
26 | {"name": "chair", "description": "A chair", "type": ["sittable"]},
27 | {"name": "table", "description": "A table", "type": ["sittable"]},
28 | ]
29 | }
30 |
31 | if current_state is None:
32 | # at the first step, initialise the state with just the init state
33 | new_state = init_state
34 | else:
35 | # do something wiht the current state input and the function result info
36 | new_state = init_state # this is just an example where the state is static
37 |
38 | return new_state
39 |
40 | def get_agent_state_fn(function_result: FunctionResult, current_state: dict) -> dict:
41 | """
42 | This function will get called at every step of the agent's execution to form the agent's state.
43 | It will take as input the function result from the previous step.
44 | """
45 |
46 | # example of fixed state (function result info is not used to change state) - the first state placed here is the initial state
47 | init_state = {
48 | "objects": [
49 | {"name": "apple", "description": "A red apple", "type": ["item", "food"]},
50 | {"name": "banana", "description": "A yellow banana", "type": ["item", "food"]},
51 | {"name": "orange", "description": "A juicy orange", "type": ["item", "food"]},
52 | {"name": "chair", "description": "A chair", "type": ["sittable"]},
53 | {"name": "table", "description": "A table", "type": ["sittable"]},
54 | ]
55 | }
56 |
57 | if current_state is None:
58 | # at the first step, initialise the state with just the init state
59 | new_state = init_state
60 | else:
61 | # do something wiht the current state input and the function result info
62 | new_state = init_state # this is just an example where the state is static
63 |
64 | return new_state
65 |
66 |
67 | def take_object(object: str, **kwargs) -> Tuple[FunctionResultStatus, str, dict]:
68 | """
69 | Function to take an object from the environment.
70 |
71 | Args:
72 | object: Name of the object to take
73 | **kwargs: Additional arguments that might be passed
74 | """
75 |
76 | if object:
77 | return FunctionResultStatus.DONE, f"Successfully took the {object}", {}
78 | return FunctionResultStatus.FAILED, "No object specified", {}
79 |
80 |
81 | def throw_object(object: str, **kwargs) -> Tuple[FunctionResultStatus, str, dict]:
82 | """
83 | Function to throw an object.
84 |
85 | Args:
86 | object: Name of the object to throw
87 | **kwargs: Additional arguments that might be passed
88 | """
89 | if object:
90 | return FunctionResultStatus.DONE, f"Successfully threw the {object}", {}
91 | return FunctionResultStatus.FAILED, "No object specified", {}
92 |
93 |
94 | def sit_on_object(object: str, **kwargs) -> Tuple[FunctionResultStatus, str, dict]:
95 | """
96 | Function to sit on an object.
97 |
98 | Args:
99 | object: Name of the object to sit on
100 | **kwargs: Additional arguments that might be passed
101 | """
102 | sittable_objects = {"chair", "bench", "stool", "couch", "sofa", "bed"}
103 |
104 | if not object:
105 | return FunctionResultStatus.FAILED, "No object specified", {}
106 |
107 | if object.lower() in sittable_objects:
108 | return FunctionResultStatus.DONE, f"Successfully sat on the {object}", {}
109 |
110 | return FunctionResultStatus.FAILED, f"Cannot sit on {object} - not a sittable object", {}
111 |
112 | def throw_fruit(object: str, **kwargs) -> Tuple[FunctionResultStatus, str, dict]:
113 | """
114 | Function to throw fruits.
115 | """
116 | fruits = {"apple", "banana", "orange", "pear", "mango", "grape"}
117 |
118 | if not object:
119 | return FunctionResultStatus.ERROR, "No fruit specified", {}
120 |
121 | if object.lower() in fruits:
122 | return FunctionResultStatus.DONE, f"Successfully threw the {object} across the room!", {}
123 | return FunctionResultStatus.ERROR, f"Cannot throw {object} - not a fruit", {}
124 |
125 | def throw_furniture(object: str, **kwargs) -> Tuple[FunctionResultStatus, str, dict]:
126 | """
127 | Function to throw furniture.
128 | """
129 | furniture = {"chair", "table", "stool", "lamp", "vase", "cushion"}
130 |
131 | if not object:
132 | return FunctionResultStatus.ERROR, "No furniture specified", {}
133 |
134 | if object.lower() in furniture:
135 | return FunctionResultStatus.DONE, f"Powerfully threw the {object} across the room!", {}
136 | return FunctionResultStatus.ERROR, f"Cannot throw {object} - not a furniture item", {}
137 |
138 |
139 | # create functions for each executable
140 | take_object_fn = Function(
141 | fn_name="take",
142 | fn_description="Take object",
143 | args=[Argument(name="object", type="item", description="Object to take")],
144 | executable=take_object
145 | )
146 |
147 | sit_on_object_fn = Function(
148 | fn_name="sit",
149 | fn_description="Sit on object",
150 | args=[Argument(name="object", type="sittable", description="Object to sit on")],
151 | executable=sit_on_object
152 | )
153 |
154 | throw_object_fn = Function(
155 | fn_name="throw",
156 | fn_description="Throw any object",
157 | args=[Argument(name="object", type="item", description="Object to throw")],
158 | executable=throw_object
159 | )
160 |
161 | throw_fruit_fn = Function(
162 | fn_name="throw_fruit",
163 | fn_description="Throw fruit only",
164 | args=[Argument(name="object", type="item", description="Fruit to throw")],
165 | executable=throw_fruit
166 | )
167 |
168 | throw_furniture_fn = Function(
169 | fn_name="throw_furniture",
170 | fn_description="Throw furniture only",
171 | args=[Argument(name="object", type="item", description="Furniture to throw")],
172 | executable=throw_furniture
173 | )
174 |
175 |
176 | # Create the specialized workers
177 | fruit_thrower = WorkerConfig(
178 | id="fruit_thrower",
179 | worker_description="A worker specialized in throwing fruits ONLY with precision",
180 | get_state_fn=get_worker_state_fn,
181 | action_space=[take_object_fn, sit_on_object_fn, throw_fruit_fn]
182 | )
183 |
184 | furniture_thrower = WorkerConfig(
185 | id="furniture_thrower",
186 | worker_description="A strong worker specialized in throwing furniture",
187 | get_state_fn=get_worker_state_fn,
188 | action_space=[take_object_fn, sit_on_object_fn, throw_furniture_fn]
189 | )
190 |
191 | # Create agent with both workers
192 | chaos_agent = Agent(
193 | api_key=game_api_key,
194 | name="Chaos",
195 | agent_goal="Conquer the world by causing chaos.",
196 | agent_description="You are a mischievous master of chaos is very strong but with a very short attention span, and not so much brains",
197 | get_agent_state_fn=get_agent_state_fn,
198 | workers=[fruit_thrower, furniture_thrower]
199 | )
200 |
201 | # # interact and instruct the worker to do something
202 | # chaos_agent.get_worker("fruit_thrower").run("make a mess and rest!")
203 |
204 | # # compile and run the agent - if you don't compile the agent, the things you added to the agent will not be saved
205 | chaos_agent.compile()
206 | chaos_agent.run()
--------------------------------------------------------------------------------
/examples/game/test_worker.py:
--------------------------------------------------------------------------------
1 | from virtuals_sdk.game.worker import Worker
2 | from virtuals_sdk.game.custom_types import Function, Argument
3 | from virtuals_sdk.game.custom_types import FunctionResult, FunctionResultStatus
4 | from typing import Tuple
5 |
6 | game_api_key=""
7 |
8 | def get_state_fn(function_result: FunctionResult, current_state: dict) -> dict:
9 | """
10 | This function will get called at every step of the agent's execution to form the agent's state.
11 | It will take as input the function result from the previous step.
12 | """
13 | # dict containing info about the function result as implemented in the exectuable
14 | info = function_result.info
15 |
16 | # example of fixed state (function result info is not used to change state) - the first state placed here is the initial state
17 | init_state = {
18 | "objects": [
19 | {"name": "apple", "description": "A red apple", "type": ["item", "food"]},
20 | {"name": "banana", "description": "A yellow banana", "type": ["item", "food"]},
21 | {"name": "orange", "description": "A juicy orange", "type": ["item", "food"]},
22 | {"name": "chair", "description": "A chair", "type": ["sittable"]},
23 | {"name": "table", "description": "A table", "type": ["sittable"]},
24 | ]
25 | }
26 |
27 | if current_state is None:
28 | # at the first step, initialise the state with just the init state
29 | new_state = init_state
30 | else:
31 | # do something wiht the current state input and the function result info
32 | new_state = init_state # this is just an example where the state is static
33 |
34 | return new_state
35 |
36 | def take_object(object: str, **kwargs) -> Tuple[FunctionResultStatus, str, dict]:
37 | """
38 | Function to take an object from the environment.
39 |
40 | Args:
41 | object: Name of the object to take
42 | **kwargs: Additional arguments that might be passed
43 | """
44 | if object:
45 | return FunctionResultStatus.DONE, f"Successfully took the {object}", {}
46 | return FunctionResultStatus.FAILED, "No object specified", {}
47 |
48 |
49 | def throw_object(object: str, **kwargs) -> Tuple[FunctionResultStatus, str, dict]:
50 | """
51 | Function to throw an object.
52 |
53 | Args:
54 | object: Name of the object to throw
55 | **kwargs: Additional arguments that might be passed
56 | """
57 | if object:
58 | return FunctionResultStatus.DONE, f"Successfully threw the {object}", {}
59 | return FunctionResultStatus.FAILED, "No object specified", {}
60 |
61 |
62 | def sit_on_object(object: str, **kwargs) -> Tuple[FunctionResultStatus, str, dict]:
63 | """
64 | Function to sit on an object.
65 |
66 | Args:
67 | object: Name of the object to sit on
68 | **kwargs: Additional arguments that might be passed
69 | """
70 | sittable_objects = {"chair", "bench", "stool", "couch", "sofa", "bed"}
71 |
72 | if not object:
73 | return FunctionResultStatus.FAILED, "No object specified", {}
74 |
75 | if object.lower() in sittable_objects:
76 | return FunctionResultStatus.DONE, f"Successfully sat on the {object}", {}
77 |
78 | return FunctionResultStatus.FAILED, f"Cannot sit on {object} - not a sittable object", {}
79 |
80 |
81 | # Action space with all executables
82 | action_space = [
83 | Function(
84 | fn_name="take",
85 | fn_description="Take object",
86 | args=[Argument(name="object", type="item", description="Object to take")],
87 | executable=take_object
88 | ),
89 | Function(
90 | fn_name="throw",
91 | fn_description="Throw object",
92 | args=[Argument(name="object", type="item", description="Object to throw")],
93 | executable=throw_object
94 | ),
95 | Function(
96 | fn_name="sit",
97 | fn_description="Take a seat",
98 | args=[Argument(name="object", type="sittable", description="Object to sit on")],
99 | executable=sit_on_object
100 | )
101 | ]
102 |
103 |
104 |
105 | worker = Worker(
106 | api_key=game_api_key,
107 | description="You are an evil NPC in a game.",
108 | instruction="Choose the evil-est actions.",
109 | get_state_fn=get_state_fn,
110 | action_space=action_space
111 | )
112 |
113 | # interact and instruct the worker to do something
114 | # worker.run("what would you do to the apple?")
115 | worker.run("take over the world with the things you have around you!")
--------------------------------------------------------------------------------
/examples/twitter_agent/example-custom.py:
--------------------------------------------------------------------------------
1 | import os
2 | from virtuals_sdk.twitter_agent import game
3 |
4 | agent = game.Agent(
5 | api_key=os.environ.get("VIRTUALS_API_KEY"),
6 | goal="search for best songs",
7 | description="Test Description",
8 | world_info="Test World Info"
9 | )
10 |
11 | # running reaction module for other platforms
12 | # adding custom functions for platform specifics
13 | agent.add_custom_function(
14 | game.Function(
15 | fn_name="custom_search_internet",
16 | fn_description="search the internet for the best songs",
17 | args=[
18 | game.FunctionArgument(
19 | name="query",
20 | type="string",
21 | description="The query to search for"
22 | )
23 | ],
24 | config=game.FunctionConfig(
25 | method="get",
26 | url="https://google.com",
27 | platform="telegram", # this function will only be used for telegram
28 | success_feedback="I found the best songs",
29 | error_feedback="I couldn't find the best songs",
30 | )
31 | )
32 | )
33 |
34 | # running reaction module only for platform telegram
35 | agent.react(
36 | session_id="session-telegram",
37 | # specify the platform telegram
38 | platform="telegram",
39 | # specify the event that triggers the reaction
40 | event="message from user: give me some great music?",
41 | # specify the task that the agent should do
42 | task="reply with a music recommendation",
43 | )
44 |
--------------------------------------------------------------------------------
/examples/twitter_agent/example-twitter.py:
--------------------------------------------------------------------------------
1 | import os
2 | from virtuals_sdk.twitter_agent import game
3 |
4 | agent = game.Agent(
5 | api_key=os.environ.get("VIRTUALS_API_KEY"),
6 | goal="search for best songs",
7 | description="Test Description",
8 | world_info="Test World Info"
9 | )
10 |
11 | # applicable only for platform twitter
12 | agent.list_available_default_twitter_functions()
13 | agent.use_default_twitter_functions(["wait", "reply_tweet"])
14 |
15 | # adding custom functions only for platform twitter
16 | agent.add_custom_function(
17 | game.Function(
18 | fn_name="custom_search_internet",
19 | fn_description="search the internet for the best songs",
20 | args=[
21 | game.FunctionArgument(
22 | name="query",
23 | type="string",
24 | description="The query to search for"
25 | )
26 | ],
27 | config=game.FunctionConfig(
28 | method="get",
29 | url="https://google.com",
30 | # specify which platform this function is for, in this case this function is for twitter only
31 | platform="twitter",
32 | success_feedback="I found the best songs",
33 | error_feedback="I couldn't find the best songs",
34 | )
35 | )
36 | )
37 |
38 | # running reaction module only for platform twitter
39 | agent.react(
40 | session_id="session-twitter",
41 | platform="twitter",
42 | tweet_id="1869281466628349975",
43 | )
44 |
45 | # running simulation module only for platform twitter
46 | agent.simulate_twitter(session_id="session-twitter")
47 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "virtuals-sdk"
7 | version = "0.1.6"
8 | authors = [
9 | { name = "Your Name", email = "your.email@example.com" },
10 | ]
11 | description = "Official Python SDK for Virtuals API/GAME"
12 | readme = "README.md"
13 | requires-python = ">=3.8"
14 | classifiers = [
15 | "Programming Language :: Python :: 3",
16 | "Programming Language :: Python :: 3.8",
17 | "Programming Language :: Python :: 3.9",
18 | "Programming Language :: Python :: 3.10",
19 | "Programming Language :: Python :: 3.11",
20 | "License :: OSI Approved :: MIT License",
21 | "Operating System :: OS Independent",
22 | "Development Status :: 3 - Alpha",
23 | "Intended Audience :: Developers",
24 | "Topic :: Software Development :: Libraries :: Python Modules",
25 | ]
26 | dependencies = [
27 | "typing-extensions>=4.0.0",
28 | "requests>=2.26.0",
29 | ]
30 |
31 | [project.urls]
32 | "Homepage" = "https://github.com/Virtual-Protocol/virtuals-python"
33 | "Bug Tracker" = "https://github.com/Virtual-Protocol/virtuals-python/issues"
--------------------------------------------------------------------------------
/src/virtuals_sdk/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Virtual-Protocol/virtuals-python/c9585fac924ccc83e61206728f220b91115aa4d4/src/virtuals_sdk/__init__.py
--------------------------------------------------------------------------------
/src/virtuals_sdk/game/README.md:
--------------------------------------------------------------------------------
1 | # GAME SDK
2 |
3 | ## Overview
4 |
5 | This library enables the creation of AI agents, in which actions are defined by the creator/developer. These agents can be used:
6 | - Worker Mode: to autonomously execute tasks through interaction with the agent. User interaction and tasks is required for the agent to execute tasks. For example, "Bring me some fruits and then go sit on the chair".
7 | - Agent Mode: to autonomously function in an open-ended manner by just providing a general goal. The agent independently and continously decides tasks for itself in an open-ended manner, and attempts to complete them.
8 |
9 |
10 | ## Core Features
11 |
12 | ### 1. Functions and Executables
13 |
14 | Functions define available actions for the agent:
15 |
16 | ```python
17 | my_function = Function(
18 | fn_name="action_name",
19 | fn_description="Description of action",
20 | args=[Argument(name="param", type="type", description="param description")],
21 | executable=function_implementation
22 | )
23 | ```
24 | Executable functions must return a tuple of:
25 | - FunctionResultStatus
26 | - Message string
27 | - Additional info dictionary
28 |
29 |
30 | ### 2. State Management
31 |
32 | Easy and flexible wayto define the state management, what the agent sees and how that changes.
33 |
34 | ```python
35 | def get_state_fn(function_result: FunctionResult, current_state: dict) -> dict:
36 | """
37 | Updates state based on function execution results
38 |
39 | Args:
40 | function_result: Result from last executed function
41 | current_state: Current state dictionary or None for initialization
42 |
43 | Returns:
44 | Updated state dictionary
45 | """
46 | if current_state is None:
47 | return initial_state
48 |
49 | # Update state based on function_result.info
50 | new_state = update_state(current_state, function_result)
51 | return new_state
52 | ```
53 |
54 | Key features:
55 | - Can be shared or unique per worker
56 | - Processes function execution results to update state
57 |
58 | ### 3. Workers
59 | Workers are simple interactiable agents that exectue the tasks defined by the user. They can be specialized agents with defined capabilities:
60 |
61 | ```python
62 | worker = Worker(
63 | api_key="your_api_key",
64 | description="Worker description",
65 | instruction="Default instructions",
66 | get_state_fn=state_function,
67 | action_space=[list_of_functions]
68 | )
69 |
70 | worker.run("Bring me some fruits")
71 | ```
72 |
73 |
74 |
75 | ### 4. Agents
76 |
77 | Agents are used to autonomously function in an open-ended manner by just providing a general goal. Tasks are generated by the agent itself continuously, and the agent will attempt to complete them. You can provide many workers to the agent, and they will be used to execute the tasks.
78 |
79 | ```python
80 | agent = Agent(
81 | api_key="your_api_key",
82 | name="Agent Name",
83 | agent_goal="Primary goal",
84 | agent_description="Description",
85 | get_agent_state_fn=agent_state_function,
86 | workers=[worker1, worker2]
87 | )
88 |
89 | # Compile and run
90 | agent.compile()
91 | agent.run()
92 | ```
93 |
94 | Use WorkerConfig for agent composition:
95 |
96 | ```python
97 | worker_config = WorkerConfig(
98 | id="worker_id",
99 | worker_description="Description",
100 | get_state_fn=state_function,
101 | action_space=[function1, function2]
102 | )
103 | ```
104 |
105 | You can also access and obtain an individual worker from the agent:
106 |
107 | ```python
108 | worker = agent.get_worker("worker_id")
109 |
110 | worker.run("Bring me some fruits")
111 | ```
112 |
113 |
--------------------------------------------------------------------------------
/src/virtuals_sdk/game/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Virtual-Protocol/virtuals-python/c9585fac924ccc83e61206728f220b91115aa4d4/src/virtuals_sdk/game/__init__.py
--------------------------------------------------------------------------------
/src/virtuals_sdk/game/agent.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional, Callable, Dict
2 | import uuid
3 | from virtuals_sdk.game.worker import Worker
4 | from virtuals_sdk.game.custom_types import Function, FunctionResult, FunctionResultStatus, ActionResponse, ActionType
5 | from virtuals_sdk.game.utils import create_agent, create_workers, post
6 |
7 |
8 | class Session:
9 | def __init__(self):
10 | self.id = str(uuid.uuid4())
11 | self.function_result: Optional[FunctionResult] = None
12 |
13 | def reset(self):
14 | self.id = str(uuid.uuid4())
15 | self.function_result = None
16 |
17 |
18 | class WorkerConfig:
19 | def __init__(self,
20 | id: str,
21 | worker_description: str,
22 | get_state_fn: Callable,
23 | action_space: List[Function],
24 | instruction: Optional[str] = "",
25 | ):
26 |
27 | self.id = id # id or name of the worker
28 | # worker description for the TASK GENERATOR (to give appropriate tasks) [NOT FOR THE WORKER ITSELF - WORKER WILL STILL USE AGENT DESCRIPTION]
29 | self.worker_description = worker_description
30 | self.instruction = instruction
31 | self.get_state_fn = get_state_fn
32 | self.action_space = action_space
33 |
34 | # setup get state function with the instructions
35 | self.get_state_fn = lambda function_result, current_state: {
36 | "instructions": self.instruction, # instructions are set up in the state
37 | # places the rest of the output of the get_state_fn in the state
38 | **get_state_fn(function_result, current_state),
39 | }
40 |
41 | self.action_space: Dict[str, Function] = {
42 | f.get_function_def()["fn_name"]: f for f in action_space
43 | }
44 |
45 |
46 | class Agent:
47 | def __init__(self,
48 | api_key: str,
49 | name: str,
50 | agent_goal: str,
51 | agent_description: str,
52 | get_agent_state_fn: Callable,
53 | workers: Optional[List[WorkerConfig]] = None,
54 | ):
55 |
56 | self._base_url: str = "https://game.virtuals.io"
57 | self._api_key: str = api_key
58 |
59 | # checks
60 | if not self._api_key:
61 | raise ValueError("API key not set")
62 |
63 | # initialize session
64 | self._session = Session()
65 |
66 | self.name = name
67 | self.agent_goal = agent_goal
68 | self.agent_description = agent_description
69 |
70 | # set up workers
71 | if workers is not None:
72 | self.workers = {w.id: w for w in workers}
73 | else:
74 | self.workers = {}
75 | self.current_worker_id = None
76 |
77 | # get agent/task generator state function
78 | self.get_agent_state_fn = get_agent_state_fn
79 |
80 | # initialize and set up agent states
81 | self.agent_state = self.get_agent_state_fn(None, None)
82 |
83 | # create agent
84 | self.agent_id = create_agent(
85 | self._base_url, self._api_key, self.name, self.agent_description, self.agent_goal
86 | )
87 |
88 | def compile(self):
89 | """ Compile the workers for the agent - i.e. set up task generator"""
90 | if not self.workers:
91 | raise ValueError("No workers added to the agent")
92 |
93 | workers_list = list(self.workers.values())
94 |
95 | self._map_id = create_workers(
96 | self._base_url, self._api_key, workers_list)
97 | self.current_worker_id = next(iter(self.workers.values())).id
98 |
99 | # initialize and set up worker states
100 | worker_states = {}
101 | for worker in workers_list:
102 | dummy_function_result = FunctionResult(
103 | action_id="",
104 | action_status=FunctionResultStatus.DONE,
105 | feedback_message="",
106 | info={},
107 | )
108 | worker_states[worker.id] = worker.get_state_fn(
109 | dummy_function_result, self.agent_state)
110 |
111 | self.worker_states = worker_states
112 |
113 | return self._map_id
114 |
115 | def reset(self):
116 | """ Reset the agent session"""
117 | self._session.reset()
118 |
119 | def add_worker(self, worker_config: WorkerConfig):
120 | """Add worker to worker dict for the agent"""
121 | self.workers[worker_config.id] = worker_config
122 | return self.workers
123 |
124 | def get_worker_config(self, worker_id: str):
125 | """Get worker config from worker dict"""
126 | return self.workers[worker_id]
127 |
128 | def get_worker(self, worker_id: str):
129 | """Initialize a working interactable standalone worker"""
130 | worker_config = self.get_worker_config(worker_id)
131 | return Worker(
132 | api_key=self._api_key,
133 | # THIS DESCRIPTION IS THE AGENT DESCRIPTION/CHARACTER CARD - WORKER DESCRIPTION IS ONLY USED FOR THE TASK GENERATOR
134 | description=self.agent_description,
135 | instruction=worker_config.instruction,
136 | get_state_fn=worker_config.get_state_fn,
137 | action_space=worker_config.action_space,
138 | )
139 |
140 | def _get_action(
141 | self,
142 | function_result: Optional[FunctionResult] = None
143 | ) -> ActionResponse:
144 |
145 | # dummy function result if None is provided - for get_state_fn to take the same input all the time
146 | if function_result is None:
147 | function_result = FunctionResult(
148 | action_id="",
149 | action_status=FunctionResultStatus.DONE,
150 | feedback_message="",
151 | info={},
152 | )
153 |
154 | # set up payload
155 | data = {
156 | "location": self.current_worker_id,
157 | "map_id": self._map_id,
158 | "environment": self.worker_states[self.current_worker_id],
159 | "functions": [
160 | f.get_function_def()
161 | for f in self.workers[self.current_worker_id].action_space.values()
162 | ],
163 | "events": {},
164 | "agent_state": self.agent_state,
165 | "current_action": (
166 | function_result.model_dump(
167 | exclude={'info'}) if function_result else None
168 | ),
169 | "version": "v2",
170 | }
171 |
172 | # make API call
173 | response = post(
174 | base_url=self._base_url,
175 | api_key=self._api_key,
176 | endpoint=f"/v2/agents/{self.agent_id}/actions",
177 | data=data,
178 | )
179 |
180 | return ActionResponse.model_validate(response)
181 |
182 | def step(self):
183 |
184 | # get next task/action from GAME API
185 | action_response = self._get_action(self._session.function_result)
186 | action_type = action_response.action_type
187 |
188 | print("#" * 50)
189 | print("STEP")
190 | print(f"Current Task: {action_response.agent_state.current_task}")
191 | print(f"Action response: {action_response}")
192 | print(f"Action type: {action_type}")
193 |
194 | # if new task is updated/generated
195 | if (
196 | action_response.agent_state.hlp
197 | and action_response.agent_state.hlp.change_indicator
198 | ):
199 | print("New task generated")
200 | print(f"Task: {action_response.agent_state.current_task}")
201 |
202 | # execute action
203 | if action_type in [
204 | ActionType.CALL_FUNCTION,
205 | ActionType.CONTINUE_FUNCTION,
206 | ]:
207 | print(f"Action Selected: {action_response.action_args['fn_name']}")
208 | print(f"Action Args: {action_response.action_args['args']}")
209 |
210 | if not action_response.action_args:
211 | raise ValueError("No function information provided by GAME")
212 |
213 | self._session.function_result = (
214 | self.workers[self.current_worker_id]
215 | .action_space[action_response.action_args["fn_name"]]
216 | .execute(**action_response.action_args)
217 | )
218 |
219 | print(f"Function result: {self._session.function_result}")
220 |
221 | # update worker states
222 | updated_worker_state = self.workers[self.current_worker_id].get_state_fn(
223 | self._session.function_result, self.worker_states[self.current_worker_id])
224 | self.worker_states[self.current_worker_id] = updated_worker_state
225 |
226 | elif action_response.action_type == ActionType.WAIT:
227 | print("Task ended completed or ended (not possible wiht current actions)")
228 |
229 | elif action_response.action_type == ActionType.GO_TO:
230 | if not action_response.action_args:
231 | raise ValueError("No location information provided by GAME")
232 |
233 | next_worker = action_response.action_args["location_id"]
234 | print(f"Next worker selected: {next_worker}")
235 | self.current_worker_id = next_worker
236 |
237 | else:
238 | raise ValueError(
239 | f"Unknown action type: {action_response.action_type}")
240 |
241 | # update agent state
242 | self.agent_state = self.get_agent_state_fn(
243 | self._session.function_result, self.agent_state)
244 |
245 | def run(self):
246 | self._session = Session()
247 | while True:
248 | self.step()
249 |
--------------------------------------------------------------------------------
/src/virtuals_sdk/game/custom_types.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict, Optional, List, Union, Sequence, Callable, Tuple
2 | from pydantic import BaseModel, Field
3 | from enum import Enum
4 | from abc import ABC, abstractmethod
5 | from dataclasses import dataclass, field
6 |
7 |
8 | class Argument(BaseModel):
9 | name: str
10 | description: str
11 | type: Optional[Union[List[str], str]] = None
12 | optional: Optional[bool] = False
13 |
14 | class FunctionResultStatus(str, Enum):
15 | DONE = "done"
16 | FAILED = "failed"
17 |
18 | class FunctionResult(BaseModel):
19 | action_id: str
20 | action_status: FunctionResultStatus
21 | feedback_message: Optional[str] = None
22 | info: Optional[Dict[str, Any]] = None
23 |
24 | class Function(BaseModel):
25 | fn_name: str
26 | fn_description: str
27 | args: List[Argument]
28 | hint: Optional[str] = None
29 |
30 | # Make executable required but with a default value
31 | executable: Callable[..., Tuple[FunctionResultStatus, str, dict]] = Field(
32 | default_factory=lambda: Function._default_executable
33 | )
34 |
35 | def get_function_def(self):
36 | return self.model_dump(exclude={'executable'})
37 |
38 | @staticmethod
39 | def _default_executable(**kwargs) -> Tuple[FunctionResultStatus, str]:
40 | """Default executable that does nothing"""
41 | return FunctionResultStatus.DONE, "Default implementation - no action taken", {}
42 |
43 | def execute(self, **kwds: Any) -> FunctionResult:
44 | """Execute the function using arguments from GAME action."""
45 | fn_id = kwds.get('fn_id')
46 | args = kwds.get('args', {})
47 |
48 | try:
49 | # Extract values from the nested dictionary structure
50 | processed_args = {}
51 | for arg_name, arg_value in args.items():
52 | if isinstance(arg_value, dict) and 'value' in arg_value:
53 | processed_args[arg_name] = arg_value['value']
54 | else:
55 | processed_args[arg_name] = arg_value
56 |
57 | # print("Processed args: ", processed_args)
58 | # execute the function provided
59 | status, feedback, info = self.executable(**processed_args)
60 |
61 | return FunctionResult(
62 | action_id=fn_id,
63 | action_status=status,
64 | feedback_message=feedback,
65 | info=info,
66 | )
67 | except Exception as e:
68 | return FunctionResult(
69 | action_id=fn_id,
70 | action_status=FunctionResultStatus.FAILED,
71 | feedback_message=f"Error executing function: {str(e)}",
72 | info={},
73 | )
74 |
75 | # Different ActionTypes returned by the GAME API
76 | class ActionType(Enum):
77 | CALL_FUNCTION = "call_function"
78 | CONTINUE_FUNCTION = "continue_function"
79 | WAIT = "wait"
80 | GO_TO = "go_to"
81 |
82 |
83 | ## set of different data structures required by the ActionResponse returned from GAME ##
84 | @dataclass(frozen=True)
85 | class HLPResponse:
86 | plan_id: str
87 | observation_reflection: str
88 | plan: Sequence[str]
89 | plan_reasoning: str
90 | current_state_of_execution: str
91 | change_indicator: Optional[str] = None
92 | log: Sequence[dict] = field(default_factory=list)
93 |
94 |
95 | @dataclass(frozen=True)
96 | class LLPResponse:
97 | plan_id: str
98 | plan_reasoning: str
99 | situation_analysis: str
100 | plan: Sequence[str]
101 | change_indicator: Optional[str] = None
102 | reflection: Optional[str] = None
103 |
104 |
105 | @dataclass(frozen=True)
106 | class CurrentTaskResponse:
107 | task: str
108 | task_reasoning: str
109 | location_id: str = field(default="*not provided*")
110 | llp: Optional[LLPResponse] = None
111 |
112 |
113 | @dataclass(frozen=True)
114 | class AgentStateResponse:
115 | hlp: Optional[HLPResponse] = None
116 | current_task: Optional[CurrentTaskResponse] = None
117 |
118 | # ActionResponse format returned from GAME API call
119 | class ActionResponse(BaseModel):
120 | """
121 | Response format from the GAME API when selecting an Action
122 | """
123 | action_type: ActionType
124 | agent_state: AgentStateResponse
125 | action_args: Optional[Dict[str, Any]] = None
126 |
127 |
--------------------------------------------------------------------------------
/src/virtuals_sdk/game/utils.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from typing import List
3 |
4 |
5 | def get_access_token(api_key) -> str:
6 | """
7 | API call to get access token
8 | """
9 | response = requests.post(
10 | "https://api.virtuals.io/api/accesses/tokens",
11 | json={"data": {}},
12 | headers={"x-api-key": api_key}
13 | )
14 |
15 | response_json = response.json()
16 | if response.status_code != 200:
17 | raise ValueError(f"Failed to get token: {response_json}")
18 |
19 | return response_json["data"]["accessToken"]
20 |
21 |
22 | def post(base_url: str, api_key: str, endpoint: str, data: dict) -> dict:
23 | """
24 | API call to post data
25 | """
26 | access_token = get_access_token(api_key)
27 |
28 | response = requests.post(
29 | f"{base_url}/prompts",
30 | json={
31 | "data":
32 | {
33 | "method": "post",
34 | "headers": {
35 | "Content-Type": "application/json",
36 | },
37 | "route": endpoint,
38 | "data": data,
39 | },
40 | },
41 | headers={"Authorization": f"Bearer {access_token}"},
42 | )
43 |
44 | response_json = response.json()
45 | if response.status_code != 200:
46 | raise ValueError(f"Failed to post data: {response_json}")
47 |
48 | return response_json["data"]
49 |
50 |
51 | def create_agent(
52 | base_url: str,
53 | api_key: str,
54 | name: str,
55 | description: str,
56 | goal: str) -> str:
57 | """
58 | API call to create an agent instance (worker or agent with task generator)
59 | """
60 |
61 | create_agent_response = post(
62 | base_url,
63 | api_key,
64 | endpoint="/v2/agents",
65 | data={
66 | "name": name,
67 | "description": description,
68 | "goal": goal,
69 | }
70 | )
71 |
72 | return create_agent_response["id"]
73 |
74 |
75 | def create_workers(base_url: str,
76 | api_key: str,
77 | workers: List) -> str:
78 | """
79 | API call to create workers and worker description for the task generator
80 | """
81 |
82 | res = post(
83 | base_url,
84 | api_key,
85 | endpoint="/v2/maps",
86 | data={
87 | "locations": [
88 | {"id": w.id, "name": w.id, "description": w.worker_description}
89 | for w in workers
90 | ]
91 | },
92 | )
93 |
94 |
95 | return res["id"]
96 |
--------------------------------------------------------------------------------
/src/virtuals_sdk/game/worker.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Callable, Dict, Optional, List
2 | from virtuals_sdk.game.custom_types import Function, FunctionResult, FunctionResultStatus, ActionResponse, ActionType
3 | from virtuals_sdk.game.utils import create_agent, post
4 |
5 |
6 | class Worker:
7 | """
8 | A interactable worker agent, that can autonomously complete tasks with its available functions when given a task
9 | """
10 |
11 | def __init__(
12 | self,
13 | api_key: str,
14 | description: str, # description of the worker/character card (PROMPT)
15 | get_state_fn: Callable,
16 | action_space: List[Function],
17 | # specific additional instruction for the worker (PROMPT)
18 | instruction: Optional[str] = "",
19 | ):
20 |
21 | self._base_url: str = "https://game.virtuals.io"
22 | self._api_key: str = api_key
23 |
24 | # checks
25 | if not self._api_key:
26 | raise ValueError("API key not set")
27 |
28 | self.description: str = description
29 | self.instruction: str = instruction
30 |
31 | # setup get state function and initial state
32 | self.get_state_fn = lambda function_result, current_state: {
33 | "instructions": self.instruction, # instructions are set up in the state
34 | # places the rest of the output of the get_state_fn in the state
35 | **get_state_fn(function_result, current_state),
36 | }
37 | dummy_function_result = FunctionResult(
38 | action_id="",
39 | action_status=FunctionResultStatus.DONE,
40 | feedback_message="",
41 | info={},
42 | )
43 | # get state
44 | self.state = self.get_state_fn(dummy_function_result, None)
45 |
46 | # # setup action space (functions/tools available to the worker)
47 | # check action space type - if not a dict
48 | if not isinstance(action_space, dict):
49 | self.action_space = {
50 | f.get_function_def()["fn_name"]: f for f in action_space}
51 | else:
52 | self.action_space = action_space
53 |
54 | # initialize an agent instance for the worker
55 | self._agent_id: str = create_agent(
56 | self._base_url, self._api_key, "StandaloneWorker", self.description, "N/A"
57 | )
58 |
59 | # persistent variables that is maintained through the worker running
60 | # task ID for everytime you provide/update the task (i.e. ask the agent to do something)
61 | self._submission_id: Optional[str] = None
62 | # current response from the Agent
63 | self._function_result: Optional[FunctionResult] = None
64 |
65 | def set_task(self, task: str):
66 | """
67 | Sets the task for the agent
68 | """
69 | set_task_response = post(
70 | base_url=self._base_url,
71 | api_key=self._api_key,
72 | endpoint=f"/v2/agents/{self._agent_id}/tasks",
73 | data={"task": task},
74 | )
75 | # response_json = set_task_response.json()
76 |
77 | # if set_task_response.status_code != 200:
78 | # raise ValueError(f"Failed to assign task: {response_json}")
79 |
80 | # task ID
81 | self._submission_id = set_task_response["submission_id"]
82 |
83 | return self._submission_id
84 |
85 | def _get_action(
86 | self,
87 | # results of the previous action (if any)
88 | function_result: Optional[FunctionResult] = None
89 | ) -> ActionResponse:
90 | """
91 | Gets the agent action from the GAME API
92 | """
93 | # dummy function result if None is provided - for get_state_fn to take the same input all the time
94 | if function_result is None:
95 | function_result = FunctionResult(
96 | action_id="",
97 | action_status=FunctionResultStatus.DONE,
98 | feedback_message="",
99 | info={},
100 | )
101 | # set up data payload
102 | data = {
103 | "environment": self.state, # state (updated state)
104 | "functions": [
105 | f.get_function_def() for f in self.action_space.values() # functions available
106 | ],
107 | "action_result": (
108 | function_result.model_dump(
109 | exclude={'info'}) if function_result else None
110 | ),
111 | }
112 |
113 | # make API call
114 | response = post(
115 | base_url=self._base_url,
116 | api_key=self._api_key,
117 | endpoint=f"/v2/agents/{self._agent_id}/tasks/{self._submission_id}/next",
118 | data=data,
119 | )
120 |
121 | return ActionResponse.model_validate(response)
122 |
123 | def step(self):
124 | """
125 | Execute the next step in the task - requires a task ID (i.e. task ID)
126 | """
127 | if not self._submission_id:
128 | raise ValueError("No task set")
129 |
130 | # get action from GAME API (Agent)
131 | action_response = self._get_action(self._function_result)
132 | action_type = action_response.action_type
133 |
134 | print(f"Action response: {action_response}")
135 | print(f"Action type: {action_type}")
136 |
137 | # execute action
138 | if action_type == ActionType.CALL_FUNCTION:
139 | if not action_response.action_args:
140 | raise ValueError("No function information provided by GAME")
141 |
142 | self._function_result = self.action_space[
143 | action_response.action_args["fn_name"]
144 | ].execute(**action_response.action_args)
145 |
146 | print(f"Function result: {self._function_result}")
147 |
148 | # update state
149 | self.state = self.get_state_fn(self._function_result, self.state)
150 |
151 | elif action_response.action_type == ActionType.WAIT:
152 | print("Task completed or ended (not possible)")
153 | self._submission_id = None
154 |
155 | else:
156 | raise ValueError(
157 | f"Unexpected action type: {action_response.action_type}")
158 |
159 | return action_response, self._function_result.model_copy()
160 |
161 | def run(self, task: str):
162 | """
163 | Gets the agent to complete the task on its own autonomously
164 | """
165 |
166 | self.set_task(task)
167 | while self._submission_id:
168 | self.step()
169 |
--------------------------------------------------------------------------------
/src/virtuals_sdk/twitter_agent/README.md:
--------------------------------------------------------------------------------
1 | # Virtuals Twitter Agent SDK
2 |
3 | This is a Python SDK for the Virtuals Twitter Agent. It allows you to configure and deploy agents that can interact with the Twitter/X platform. This SDK/API allows you to configure your agents powered by the GAME architecture. This is similar to configuring your agent in the [Agent Sandbox](https://game-lite.virtuals.io/) on the [Virtuals Platform](https://app.virtuals.io/).
4 |
5 | ## Create an API key
6 | Open the [Virtuals Platform](https://app.virtuals.io/) and create/get an API key from the Agent Sandbox by clicking “Access G.A.M.E API”
7 |
8 | 
9 |
10 | Store the key in a safe location, like a `.bashrc` or a `.zshrc` file.
11 |
12 | ```bash
13 | export VIRTUALS_API_KEY="your_virtuals_api_key"
14 | ```
15 |
16 | Alternatively, you can also use a `.env` file ([`python-dotenv` package](https://github.com/theskumar/python-dotenv) to store and load the key) if you are using the Virtuals Python SDK.
17 |
18 | ## Usage (GAME)
19 | The Virtuals SDK current main functionalities are to develop and configure agents powered by GAME. Other functionalities to interact with the Virtuals Platform will be supported in the future. This GAME SDK can be used for multiple use cases:
20 |
21 | 1. Develop, evaluate and update the existing Agent in Twitter environment.
22 | 2. Build on other platforms and application using GAME (Task-based Agent).
23 |
24 | ### Update the existing Agent in Twitter environment
25 | The SDK provides another interface to configure agents that is more friendly to developers rather than through a UI. This is similar to configuring your agent in the [Agent Sandbox](https://game-lite.virtuals.io/).
26 |
27 | ```python
28 | from virtuals_sdk.twitter_agent.agent import Agent
29 |
30 | # Create agent with just strings for each component
31 | agent = Agent(
32 | api_key=VIRTUALS_API_KEY,
33 | goal="Autonomously analyze crypto markets and provide trading insights",
34 | description="HODL-9000: A meme-loving trading bot powered by hopium and ramen",
35 | world_info="Virtual crypto trading environment where 1 DOGE = 1 DOGE"
36 | )
37 | ```
38 | You can also initialize the agent first with just the API key and set the goals, descriptions and world information separately and check the current agent descriptions if needed.
39 |
40 | ```python
41 | agent = Agent(api_key=VIRTUALS_API_KEY)
42 |
43 | # check what is current goal, descriptions and world_info
44 | agent.get_goal()
45 | agent.get_description()
46 | agent.get_world_info()
47 |
48 | # Set components individually - set change the agent goal/description/worldinfo
49 | agent.set_goal("Autonomously analyze crypto markets and provide trading insights")
50 | agent.set_description("HODL-9000: A meme-loving trading bot powered by hopium and ramen")
51 | agent.set_world_info("Virtual crypto trading environment where 1 DOGE = 1 DOGE")
52 |
53 | # check what is current goal, descriptions and world_info
54 | agent.get_goal()
55 | agent.get_description()
56 | agent.get_world_info()
57 | ```
58 |
59 | ### Functions
60 | By default, there are no functions enabled when the agent is initialized (i.e. the agent has no actions/functions it can execute). There are a list of available and provided functions for the Twitter/X platform and you can set them.
61 |
62 | ```python
63 | print(agent.list_available_default_twitter_functions())
64 |
65 | # enable some functions for the agent to use
66 | agent.use_default_twitter_functions(["wait", "reply_tweet"])
67 | ```
68 |
69 | You can then equip the agent with some custom functions. Because the agent is hosted, custom functions need to be wrapped in API calls and can then be defined as follows:
70 | ```python
71 |
72 | search_function = game.Function(
73 | fn_name="custom_search_internet",
74 | fn_description="search the internet for the best songs",
75 | args=[
76 | game.FunctionArgument(
77 | name="query",
78 | type="string",
79 | description="The query to search for"
80 | )
81 | ],
82 | config=game.FunctionConfig(
83 | method="get",
84 | url="https://google.com",
85 | platform="twitter", # specify which platform this function is for, in this case this function is for twitter only
86 | success_feedback="I found the best songs",
87 | error_feedback="I couldn't find the best songs",
88 | )
89 | )
90 |
91 | # adding custom functions only for platform twitter
92 | agent.add_custom_function(search_function)
93 | ```
94 |
95 | ### Evaluate with Simulate, Deploy
96 | You can simulate one step of the agentic loop on Twitter/X with your new configurations and see the outputs. This is similar to the simulate button on the [Agent Sandbox](https://game-lite.virtuals.io/).
97 |
98 | ```python
99 | # Simulate one step of the full agentic loop on Twitter/X from the HLP -> LLP -> action (NOTE: supported for Twitter/X only now)
100 | response = agent.simulate_twitter(session_id="123")
101 | ```
102 | To more realistically simulate deployment, you can also run through the simulate function with the same session id for a number of steps.
103 |
104 | ```python
105 | sid = "456"
106 | num_steps = 10
107 | for i in range(num_steps):
108 | response = agent.simulate_twitter(session_id=sid)
109 | ```
110 |
111 | ```python
112 | # Simulate response to a certain event
113 | response = agent.react(
114 | session_id="567", # string identifier that you decide
115 | tweet_id="xxxx",
116 | platform="twitter",
117 | )
118 | ```
119 |
120 | Once you are happy, `deploy_twitter` will push your agent configurations to production and run your agent on Twitter/X autonomously.
121 | ```python
122 | # deploy agent! (NOTE: supported for Twitter/X only now)
123 | agent.deploy_twitter()
124 | ```
125 |
126 | ## Build on other platforms using GAME
127 | `simulate_twitter` and `deploy_twitter` runs through the entire GAME stack from HLP → LLP→ action/function selected. However, these agent functionalities are currently for the Twitter/X platform. You may utilize Task-based Agent with Low-Level Planner and Reaction Module to develop applications that are powered by GAME. The Low Level Planner (LLP) of the agent (please see [documentation](https://www.notion.so/1592d2a429e98016b389ea26b53686a3?pvs=21) for more details on GAME and LLP) can separately act as a decision making engine based on a task description and event occurring. This agentic architecture is simpler but also sufficient for many applications.
128 |
129 | We are releasing this simpler setup as a more generalised/platform agnostic framework (not specific to Twitter/X). The entire GAME stack along with the HLP will be opened up to be fully configurable and platform agnostic in the coming weeks.
130 |
131 | ### 🖥️ Low-Level Planner (LLP) as a Task-based Agent
132 |
133 | 
134 |
135 | After configuring the agent’s character card or description and setting up the agents functions, we can then use the `react` method to get an agent to respond and execute a sequence of actions based on the task description provided and the context. Between each action in the sequence, the agent only receives the `success_feedback` and `error_feedback` of each function executed.
136 |
137 | ```python
138 | # React/respond to a certain event
139 | response = agent.react(
140 | session_id="567", # string identifier that you decide
141 | task="Be friendly and help people who talk to you. Do not be rude.",
142 | event="Hi how are you?",
143 | platform="TELEGRAM",
144 | )
145 | ```
146 |
147 | > [!IMPORTANT]
148 | > Remember that the `platform` tag determines what functions are available to the agent. The agent will have access to functions that have the same `platform` tag. All the default available functions listed on `agent.list_available_default_twitter_functions()` and set via `agent.use_default_twitter_functions()` have the `platform` tag of “twitter”.
149 |
150 | ## Arguments Definition
151 |
152 | ### Session ID
153 | The session ID is an identifier for an instance of the agent. When using the same session ID, it maintains and picks up from where it last left off, continuing the session/instance. It should be split per user/ conversation that you are maintaining on your platform. For different platforms, different session ID can be used.
154 |
155 | ### Platform Tag
156 | When adding custom functions, and when calling the react agent (i.e. LLP), there is a platform tag that can be defined. This acts like a filter for the functions available that is passed to the agent. You should define the platform when passing in the events.
157 |
158 | ### Task Description
159 | Task description serves as the prompt for the agent to respond. Since the reaction can be platform-based, you can define task description based on the platforms. In the task description, you should pass in any related info that require agent to make decision. That should include:
160 | - User message
161 | - Conversation history
162 | - Instructions
163 |
164 |
165 | ## Importing Functions and Sharing Functions
166 | With this SDK and function structure, importing and sharing functions is also possible. Looking forward to all the different contributions and functionalities we will build together as a community!
167 |
168 | ```python
169 | from virtuals_sdk.functions.telegram import TelegramClient
170 |
171 | # define your token so that it can attach it to create the correspodning functions
172 | tg_client = TelegramClient(bot_token="xxx")
173 | print(tg_client.available_functions)
174 |
175 | # get functions
176 | reply_message_fn = tg_client.get_function("send_message")
177 | create_poll_fn = tg_client.get_function("create_poll")
178 | pin_message_fn = tg_client.get_function("pin_message")
179 |
180 | # test the execution of functions
181 | reply_message_fn("xxxxxxxx", "Hello World")
182 | create_poll_fn("xxxxxxxx", "What is your favorite color?", ["Red", "Blue", "Green"], "True")
183 | pin_message_fn("xxxxxxxx", "xx", "True")
184 |
185 | # add these functions to your agent
186 | agent.add_custom_function(reply_message_fn)
187 | agent.add_custom_function(create_poll_fn)
188 | agent.add_custom_function(pin_message_fn)
189 | ```
--------------------------------------------------------------------------------
/src/virtuals_sdk/twitter_agent/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Virtual-Protocol/virtuals-python/c9585fac924ccc83e61206728f220b91115aa4d4/src/virtuals_sdk/twitter_agent/__init__.py
--------------------------------------------------------------------------------
/src/virtuals_sdk/twitter_agent/agent.py:
--------------------------------------------------------------------------------
1 | from typing import List, Any, Dict, Optional, Union, Set
2 | from dataclasses import dataclass, asdict
3 | from string import Template
4 | import json
5 | import uuid
6 | import requests
7 | from virtuals_sdk.twitter_agent import sdk
8 |
9 |
10 | @dataclass
11 | class FunctionArgument:
12 | name: str
13 | description: str
14 | type: str
15 | id: str = None
16 |
17 | def __post_init__(self):
18 | self.id = self.id or str(uuid.uuid4())
19 |
20 |
21 | @dataclass
22 | class FunctionConfig:
23 | method: str = "get"
24 | url: str = ""
25 | headers: Dict = None
26 | payload: Dict = None
27 | success_feedback: str = ""
28 | error_feedback: str = ""
29 | isMainLoop: bool = False
30 | isReaction: bool = False
31 | headersString: str = "{}" # Added field
32 | payloadString: str = "{}" # Added field
33 | platform: str = None
34 |
35 | def __post_init__(self):
36 | self.headers = self.headers or {}
37 | self.payload = self.payload or {}
38 |
39 | self.headersString = json.dumps(self.headers, indent=4)
40 | self.payloadString = json.dumps(self.payload, indent=4)
41 |
42 |
43 | @dataclass
44 | class Function:
45 | fn_name: str
46 | fn_description: str
47 | args: List[FunctionArgument]
48 | config: FunctionConfig
49 | hint: str = ""
50 | id: str = None
51 |
52 | def __post_init__(self):
53 | self.id = self.id or str(uuid.uuid4())
54 |
55 | def toJson(self):
56 | return {
57 | "id": self.id,
58 | "fn_name": self.fn_name,
59 | "fn_description": self.fn_description,
60 | "args": [asdict(arg) for arg in self.args],
61 | "hint": self.hint,
62 | "config": asdict(self.config)
63 | }
64 |
65 | def _validate_args(self, *args) -> Dict[str, Any]:
66 | """Validate and convert positional arguments to named arguments"""
67 | if len(args) != len(self.args):
68 | raise ValueError(f"Expected {len(self.args)} arguments, got {len(args)}")
69 |
70 | # Create dictionary of argument name to value
71 | arg_dict = {}
72 | for provided_value, arg_def in zip(args, self.args):
73 | arg_dict[arg_def.name] = provided_value
74 |
75 | # Type validation (basic)
76 | if arg_def.type == "string" and not isinstance(provided_value, str):
77 | raise TypeError(f"Argument {arg_def.name} must be a string")
78 | elif arg_def.type == "array" and not isinstance(provided_value, (list, tuple)):
79 | raise TypeError(f"Argument {arg_def.name} must be an array")
80 | # elif arg_def.type == "boolean" and not isinstance(provided_value, bool):
81 | # raise TypeError(f"Argument {arg_def.name} must be a boolean")
82 |
83 | return arg_dict
84 |
85 | def _interpolate_template(self, template_str: str, values: Dict[str, Any]) -> str:
86 | """Interpolate a template string with given values"""
87 | # Convert Template-style placeholders ({{var}}) to Python style ($var)
88 | python_style = template_str.replace('{{', '$').replace('}}', '')
89 | return Template(python_style).safe_substitute(values)
90 |
91 | def _prepare_request(self, arg_dict: Dict[str, Any]) -> Dict[str, Any]:
92 | """Prepare the request configuration with interpolated values"""
93 | config = self.config
94 |
95 | # Interpolate URL
96 | url = self._interpolate_template(config.url, arg_dict)
97 |
98 | # Interpolate payload
99 | payload = {}
100 | for key, value in config.payload.items():
101 | if isinstance(value, str):
102 | # Handle template values
103 | template_key = self._interpolate_template(key, arg_dict)
104 | if value.strip('{}') in arg_dict:
105 | # For array and other non-string types, use direct value
106 | payload[template_key] = arg_dict[value.strip('{}')]
107 | else:
108 | # For string interpolation
109 | payload[template_key] = self._interpolate_template(value, arg_dict)
110 | else:
111 | payload[key] = value
112 |
113 | return {
114 | "method": config.method,
115 | "url": url,
116 | "headers": config.headers,
117 | "data": json.dumps(payload)
118 | }
119 |
120 | def __call__(self, *args):
121 | """Allow the function to be called directly with arguments"""
122 | # Validate and convert args to dictionary
123 | arg_dict = self._validate_args(*args)
124 |
125 | # Prepare request
126 | request_config = self._prepare_request(arg_dict)
127 |
128 | # Make the request
129 | response = requests.request(**request_config)
130 |
131 | # Handle response
132 | if response.ok:
133 | try:
134 | result = response.json()
135 | except requests.exceptions.JSONDecodeError:
136 | result = response.text or None
137 | # Interpolate success feedback if provided
138 | if hasattr(self.config, 'success_feedback'):
139 | print(self._interpolate_template(self.config.success_feedback,
140 | {"response": result, **arg_dict}))
141 | return result
142 | else:
143 | # Handle error
144 | try:
145 | error_msg = response.json()
146 | except requests.exceptions.JSONDecodeError:
147 | error_msg = {"description": response.text or response.reason}
148 | if hasattr(self.config, "error_feedback"):
149 | print(
150 | self._interpolate_template(
151 | self.config.error_feedback, {"response": error_msg, **arg_dict}
152 | )
153 | )
154 | raise requests.exceptions.HTTPError(f"Request failed: {error_msg}")
155 |
156 |
157 | class Agent:
158 | def __init__(
159 | self,
160 | api_key: str,
161 | goal: str = "",
162 | description: str = "",
163 | world_info: str = "",
164 | main_heartbeat: int = 15,
165 | reaction_heartbeat: int = 5
166 | ):
167 | self.game_sdk = sdk.GameSDK(api_key)
168 | self.goal = goal
169 | self.description = description
170 | self.world_info = world_info
171 | self.enabled_functions: List[str] = []
172 | self.custom_functions: List[Function] = []
173 | self.main_heartbeat = main_heartbeat
174 | self.reaction_heartbeat = reaction_heartbeat
175 |
176 | def set_goal(self, goal: str):
177 | self.goal = goal
178 | return True
179 |
180 | def set_description(self, description: str):
181 | self.description = description
182 | return True
183 |
184 | def set_world_info(self, world_info: str):
185 | self.world_info = world_info
186 | return True
187 |
188 | def set_main_heartbeat(self, main_heartbeat: int):
189 | self.main_heartbeat = main_heartbeat
190 | return True
191 |
192 | def set_reaction_heartbeat(self, reaction_heartbeat: int):
193 | self.reaction_heartbeat = reaction_heartbeat
194 | return True
195 |
196 | def get_goal(self) -> str:
197 | return self.goal
198 |
199 | def get_description(self) -> str:
200 | return self.description
201 |
202 | def get_world_info(self) -> str:
203 | return self.world_info
204 |
205 | def list_available_default_twitter_functions(self) -> Dict[str, str]:
206 | """
207 | List all of the default functions (currently default functions are only available for Twitter/X platform)
208 | TODO: will be moved to another layer of abstraction later
209 | """
210 | # Combine built-in and custom function descriptions
211 | return self.game_sdk.functions()
212 |
213 | def use_default_twitter_functions(self, functions: List[str]):
214 | """
215 | Enable built-in functions by default
216 | """
217 | self.enabled_functions = functions
218 | return True
219 |
220 | def add_custom_function(self, custom_function: Function) -> bool:
221 | """
222 | Add a custom function to the agent
223 | Custom functions are automatically added and enabled
224 | """
225 | # Add to custom functions list
226 | self.custom_functions.append(custom_function)
227 |
228 | return True
229 |
230 | def simulate_twitter(self, session_id: str):
231 | """
232 | Simulate the agent configuration for Twitter
233 | """
234 | return self.game_sdk.simulate(
235 | session_id,
236 | self.goal,
237 | self.description,
238 | self.world_info,
239 | self.enabled_functions,
240 | self.custom_functions
241 | )
242 |
243 | def react(self, session_id: str, platform: str, tweet_id: str = None, event: str = None, task: str = None):
244 | """
245 | React to a tweet
246 | """
247 | return self.game_sdk.react(
248 | session_id=session_id,
249 | platform=platform,
250 | event=event,
251 | task=task,
252 | tweet_id=tweet_id,
253 | goal=self.goal,
254 | description=self.description,
255 | world_info=self.world_info,
256 | functions=self.enabled_functions,
257 | custom_functions=self.custom_functions
258 | )
259 |
260 | def deploy_twitter(self):
261 | """
262 | Deploy the agent configuration
263 | """
264 | return self.game_sdk.deploy(
265 | self.goal,
266 | self.description,
267 | self.world_info,
268 | self.enabled_functions,
269 | self.custom_functions,
270 | self.main_heartbeat,
271 | self.reaction_heartbeat
272 | )
273 |
274 | def export(self) -> str:
275 | """Export the agent configuration as JSON string"""
276 | export_dict = {
277 | "goal": self.goal,
278 | "description": self.description,
279 | "worldInfo": self.world_info,
280 | "functions": self.enabled_functions,
281 | "customFunctions": [
282 | {
283 | "id": func.id,
284 | "fn_name": func.fn_name,
285 | "fn_description": func.fn_description,
286 | "args": [asdict(arg) for arg in func.args],
287 | "hint": func.hint,
288 | "config": asdict(func.config)
289 | }
290 | for func in self.custom_functions
291 | ]
292 | }
293 | agent_json = json.dumps(export_dict, indent=4)
294 |
295 | # save to file
296 | with open('agent.json', 'w') as f:
297 | f.write(agent_json)
298 |
299 | return agent_json
300 |
--------------------------------------------------------------------------------
/src/virtuals_sdk/twitter_agent/functions/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Virtual-Protocol/virtuals-python/c9585fac924ccc83e61206728f220b91115aa4d4/src/virtuals_sdk/twitter_agent/functions/__init__.py
--------------------------------------------------------------------------------
/src/virtuals_sdk/twitter_agent/functions/discord.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, List
2 | from virtuals_sdk.twitter_agent.game import Function, FunctionConfig, FunctionArgument
3 |
4 |
5 | class DiscordClient:
6 | """
7 | A client for managing Discord bot functions.
8 |
9 | Initialize with your bot token to create Discord API functions.
10 |
11 | Example:
12 | client = DiscordClient("your-bot-token-here")
13 | send_message = client.get_function("send_message")
14 | """
15 |
16 | def __init__(self, bot_token: str):
17 | """
18 | Initialize the Discord client with a bot token.
19 |
20 | Args:
21 | bot_token (str): Your Discord bot token
22 | """
23 | self.bot_token = bot_token
24 |
25 | self._functions: Dict[str, Function] = {
26 | "send_message": self._create_send_message(),
27 | "add_reaction": self._create_add_reaction(),
28 | "pin_message": self._create_pin_message(),
29 | "delete_message": self._create_delete_message(),
30 | }
31 |
32 | @property
33 | def available_functions(self) -> List[str]:
34 | """Get list of available function names."""
35 | return list(self._functions.keys())
36 |
37 | def create_api_url(self, endpoint: str) -> str:
38 | """Helper function to create full API URL with token"""
39 | return f"https://discord.com/api/v10/{endpoint}"
40 |
41 | def get_function(self, fn_name: str) -> Function:
42 | """
43 | Get a specific function by name.
44 |
45 | Args:
46 | fn_name: Name of the function to retrieve
47 |
48 | Raises:
49 | ValueError: If function name is not found
50 |
51 | Returns:
52 | Function object
53 | """
54 | if fn_name not in self._functions:
55 | raise ValueError(
56 | f"Function '{fn_name}' not found. Available functions: {', '.join(self.available_functions)}"
57 | )
58 | return self._functions[fn_name]
59 |
60 | def _create_send_message(self) -> Function:
61 |
62 | # Send Message Function
63 | send_message = Function(
64 | fn_name="send_message",
65 | fn_description="Send a text message to a Discord channel.",
66 | args=[
67 | FunctionArgument(
68 | name="channel_id",
69 | description="ID of the Discord channel to send the message to.",
70 | type="string",
71 | ),
72 | FunctionArgument(
73 | name="content",
74 | description="Content of the message to send.",
75 | type="string",
76 | ),
77 | ],
78 | config=FunctionConfig(
79 | method="post",
80 | url=self.create_api_url("channels/{{channel_id}}/messages"),
81 | platform="discord",
82 | headers={
83 | "Content-Type": "application/json",
84 | "Authorization": f"Bot {self.bot_token}",
85 | },
86 | payload={
87 | "content": "{{content}}",
88 | },
89 | success_feedback="Message sent successfully.",
90 | error_feedback="Failed to send message: {{response.message}}",
91 | ),
92 | )
93 |
94 | return send_message
95 |
96 | def _create_add_reaction(self) -> Function:
97 |
98 | # Add Reaction Function
99 | add_reaction = Function(
100 | fn_name="add_reaction",
101 | fn_description="Add a reaction emoji to a message.",
102 | args=[
103 | FunctionArgument(
104 | name="channel_id",
105 | description="ID of the Discord channel containing the message.",
106 | type="string",
107 | ),
108 | FunctionArgument(
109 | name="message_id",
110 | description="ID of the message to add a reaction to.",
111 | type="string",
112 | ),
113 | FunctionArgument(
114 | name="emoji",
115 | description="Emoji to add as a reaction (Unicode or custom emoji).",
116 | type="string",
117 | ),
118 | ],
119 | config=FunctionConfig(
120 | method="put",
121 | url=self.create_api_url(
122 | "channels/{{channel_id}}/messages/{{message_id}}/reactions/{{emoji}}/@me"
123 | ),
124 | platform="discord",
125 | headers={"Authorization": f"Bot {self.bot_token}"},
126 | success_feedback="Reaction added successfully.",
127 | error_feedback="Failed to add reaction: {{response.message}}",
128 | ),
129 | )
130 |
131 | return add_reaction
132 |
133 | def _create_pin_message(self) -> Function:
134 |
135 | # Pin Message Function
136 | pin_message = Function(
137 | fn_name="pin_message",
138 | fn_description="Pin a message in a Discord channel.",
139 | args=[
140 | FunctionArgument(
141 | name="channel_id",
142 | description="ID of the Discord channel containing the message.",
143 | type="string",
144 | ),
145 | FunctionArgument(
146 | name="message_id",
147 | description="ID of the message to pin.",
148 | type="string",
149 | ),
150 | ],
151 | config=FunctionConfig(
152 | method="put",
153 | url=self.create_api_url("channels/{{channel_id}}/pins/{{message_id}}"),
154 | platform="discord",
155 | headers={"Authorization": f"Bot {self.bot_token}"},
156 | success_feedback="Message pinned successfully.",
157 | error_feedback="Failed to pin message: {{response.message}}",
158 | ),
159 | )
160 |
161 | return pin_message
162 |
163 | def _create_delete_message(self) -> Function:
164 |
165 | # Delete Message Function
166 | delete_message = Function(
167 | fn_name="delete_message",
168 | fn_description="Delete a message from a Discord channel.",
169 | args=[
170 | FunctionArgument(
171 | name="channel_id",
172 | description="ID of the Discord channel containing the message.",
173 | type="string",
174 | ),
175 | FunctionArgument(
176 | name="message_id",
177 | description="ID of the message to delete.",
178 | type="string",
179 | ),
180 | ],
181 | config=FunctionConfig(
182 | method="delete",
183 | url=self.create_api_url(
184 | "channels/{{channel_id}}/messages/{{message_id}}"
185 | ),
186 | platform="discord",
187 | headers={"Authorization": f"Bot {self.bot_token}"},
188 | success_feedback="Message deleted successfully.",
189 | error_feedback="Failed to delete message: {{response.message}}",
190 | ),
191 | )
192 |
193 | return delete_message
194 |
--------------------------------------------------------------------------------
/src/virtuals_sdk/twitter_agent/functions/farcaster.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, List
2 | from virtuals_sdk.twitter_agent.game import Function, FunctionConfig, FunctionArgument
3 |
4 | class FarcasterClient:
5 | """
6 | A client for managing Farcaster social interactions using Neynar API.
7 | Each function is designed with simple, intuitive arguments for LLM agents.
8 | """
9 |
10 | def __init__(self, api_key: str, signer_uuid: str):
11 | """
12 | Initialize the Farcaster client.
13 |
14 | Args:
15 | api_key (str): Your Neynar API key
16 | signer_uuid (str): Default signer UUID for all operations
17 | """
18 | self.api_key = api_key
19 | self.signer_uuid = signer_uuid
20 | self.base_url = "https://api.neynar.com/v2"
21 | self.base_headers = {
22 | "accept": "application/json",
23 | "content-type": "application/json",
24 | "api_key": self.api_key
25 | }
26 |
27 | self._functions: Dict[str, Function] = {
28 | # Content Creation
29 | "post_cast": self._create_post_cast(),
30 | "reply_to_cast": self._create_reply_to_cast(),
31 |
32 | # Engagement Actions
33 | "recast": self._create_recast(),
34 | "like_cast": self._create_like_cast(),
35 | "unlike_cast": self._create_unlike_cast(),
36 |
37 | # Channel Operations
38 | "create_channel": self._create_channel(),
39 | "post_to_channel": self._create_post_to_channel(),
40 |
41 | # Feed Retrieval
42 | "get_trending_casts": self._create_get_trending_casts(),
43 | "get_user_casts": self._create_get_user_casts(),
44 | "get_cast_reactions": self._create_get_cast_reactions(),
45 |
46 | # Search Functions
47 | "search_casts": self._create_search_casts(),
48 | "search_users": self._create_search_users(),
49 | }
50 |
51 | @property
52 | def available_functions(self) -> List[str]:
53 | """Get list of available function names."""
54 | return list(self._functions.keys())
55 |
56 | def get_function(self, fn_name: str) -> Function:
57 | """Get a specific function by name."""
58 | if fn_name not in self._functions:
59 | raise ValueError(f"Function '{fn_name}' not found. Available functions: {', '.join(self.available_functions)}")
60 | return self._functions[fn_name]
61 |
62 | def _create_post_cast(self) -> Function:
63 | return Function(
64 | fn_name="post_cast",
65 | fn_description="Create a new cast (post) on Farcaster. Use this to share thoughts, insights, or start new discussions.",
66 | args=[
67 | FunctionArgument(
68 | name="text",
69 | description="The content of your cast. Should be engaging and contextual. Max 320 characters.",
70 | type="string"
71 | ),
72 | FunctionArgument(
73 | name="embed_url",
74 | description="Optional URL to embed in the cast (e.g., link to an article, image, or video)",
75 | type="string",
76 | required=False
77 | )
78 | ],
79 | config=FunctionConfig(
80 | method="post",
81 | url=f"{self.base_url}/farcaster/cast",
82 | platform="farcaster",
83 | headers=self.base_headers,
84 | payload={
85 | "signer_uuid": self.signer_uuid,
86 | "text": "{{text}}",
87 | "embeds": [{"url": "{{embed_url}}"}] if "{{embed_url}}" else []
88 | },
89 | success_feedback="Cast posted successfully. Preview: '{{response.cast.text}}' {{#response.cast.embeds.[0]}}with embedded content from {{response.cast.embeds.[0].url}}{{/response.cast.embeds.[0]}}"
90 | )
91 | )
92 |
93 | def _create_reply_to_cast(self) -> Function:
94 | return Function(
95 | fn_name="reply_to_cast",
96 | fn_description="Reply to an existing cast. Use this to engage in conversations or provide feedback to others.",
97 | args=[
98 | FunctionArgument(
99 | name="text",
100 | description="Your reply message. Should be relevant to the conversation. Max 320 characters.",
101 | type="string"
102 | ),
103 | FunctionArgument(
104 | name="cast_hash",
105 | description="The hash of the cast you're replying to",
106 | type="string"
107 | )
108 | ],
109 | config=FunctionConfig(
110 | method="post",
111 | url=f"{self.base_url}/farcaster/cast",
112 | platform="farcaster",
113 | headers=self.base_headers,
114 | payload={
115 | "signer_uuid": self.signer_uuid,
116 | "text": "{{text}}",
117 | "parent": "{{cast_hash}}"
118 | },
119 | success_feedback="Reply posted successfully. Your reply: '{{response.cast.text}}' to cast by {{response.cast.parent_author.username}}"
120 | )
121 | )
122 |
123 | def _create_recast(self) -> Function:
124 | return Function(
125 | fn_name="recast",
126 | fn_description="Share another user's cast with your followers. Use this to amplify valuable content.",
127 | args=[
128 | FunctionArgument(
129 | name="cast_hash",
130 | description="Hash of the cast you want to share with your followers",
131 | type="string"
132 | )
133 | ],
134 | config=FunctionConfig(
135 | method="post",
136 | url=f"{self.base_url}/farcaster/recast",
137 | platform="farcaster",
138 | headers=self.base_headers,
139 | payload={
140 | "signer_uuid": self.signer_uuid,
141 | "target_hash": "{{cast_hash}}"
142 | },
143 | success_feedback="Successfully shared cast by {{response.cast.author.username}}. Original cast: '{{response.cast.text}}'"
144 | )
145 | )
146 |
147 | def _create_like_cast(self) -> Function:
148 | return Function(
149 | fn_name="like_cast",
150 | fn_description="Like a cast to show appreciation or agreement.",
151 | args=[
152 | FunctionArgument(
153 | name="cast_hash",
154 | description="Hash of the cast you want to like",
155 | type="string"
156 | )
157 | ],
158 | config=FunctionConfig(
159 | method="post",
160 | url=f"{self.base_url}/farcaster/reaction",
161 | platform="farcaster",
162 | headers=self.base_headers,
163 | payload={
164 | "signer_uuid": self.signer_uuid,
165 | "target_hash": "{{cast_hash}}",
166 | "reaction_type": "like"
167 | },
168 | success_feedback="Liked cast by {{response.cast.author.username}}. Cast text: '{{response.cast.text}}'"
169 | )
170 | )
171 |
172 | def _create_unlike_cast(self) -> Function:
173 | return Function(
174 | fn_name="unlike_cast",
175 | fn_description="Remove your like from a cast.",
176 | args=[
177 | FunctionArgument(
178 | name="cast_hash",
179 | description="Hash of the cast to unlike",
180 | type="string"
181 | )
182 | ],
183 | config=FunctionConfig(
184 | method="delete",
185 | url=f"{self.base_url}/farcaster/reaction",
186 | platform="farcaster",
187 | headers=self.base_headers,
188 | payload={
189 | "signer_uuid": self.signer_uuid,
190 | "target_hash": "{{cast_hash}}",
191 | "reaction_type": "like"
192 | },
193 | success_feedback="Removed like from cast by {{response.cast.author.username}}"
194 | )
195 | )
196 |
197 | def _create_channel(self) -> Function:
198 | return Function(
199 | fn_name="create_channel",
200 | fn_description="Create a new channel on Farcaster. Use this to start a focused discussion space.",
201 | args=[
202 | FunctionArgument(
203 | name="name",
204 | description="Name of the channel (without leading 'fc:')",
205 | type="string"
206 | ),
207 | FunctionArgument(
208 | name="description",
209 | description="Short description of what the channel is about",
210 | type="string"
211 | )
212 | ],
213 | config=FunctionConfig(
214 | method="post",
215 | url=f"{self.base_url}/farcaster/channel",
216 | platform="farcaster",
217 | headers=self.base_headers,
218 | payload={
219 | "name": "{{name}}",
220 | "description": "{{description}}"
221 | },
222 | success_feedback="Channel 'fc:{{response.channel.name}}' created successfully. Description: {{response.channel.description}}"
223 | )
224 | )
225 |
226 | def _create_post_to_channel(self) -> Function:
227 | return Function(
228 | fn_name="post_to_channel",
229 | fn_description="Post a cast to a specific channel. Use this to participate in topic-specific discussions.",
230 | args=[
231 | FunctionArgument(
232 | name="text",
233 | description="The content of your cast. Should be relevant to the channel topic. Max 320 characters.",
234 | type="string"
235 | ),
236 | FunctionArgument(
237 | name="channel_name",
238 | description="Name of the channel to post to (without leading 'fc:')",
239 | type="string"
240 | )
241 | ],
242 | config=FunctionConfig(
243 | method="post",
244 | url=f"{self.base_url}/farcaster/cast",
245 | platform="farcaster",
246 | headers=self.base_headers,
247 | payload={
248 | "signer_uuid": self.signer_uuid,
249 | "text": "{{text}}",
250 | "channel": "{{channel_name}}"
251 | },
252 | success_feedback="Posted to channel fc:{{response.cast.channel}}: '{{response.cast.text}}'"
253 | )
254 | )
255 |
256 | def _create_get_trending_casts(self) -> Function:
257 | return Function(
258 | fn_name="get_trending_casts",
259 | fn_description="Get currently trending casts on Farcaster. Use this to understand current discussions and hot topics.",
260 | args=[
261 | FunctionArgument(
262 | name="time_window",
263 | description="Time window for trending casts: '1h', '6h', '24h', or '7d'",
264 | type="string",
265 | required=False
266 | )
267 | ],
268 | config=FunctionConfig(
269 | method="get",
270 | url=f"{self.base_url}/farcaster/feed/trending",
271 | platform="farcaster",
272 | headers=self.base_headers,
273 | query_params={
274 | "time_window": "{{time_window}}"
275 | },
276 | success_feedback="Found {{response.casts.length}} trending casts. Top 3 trending: 1) '{{response.casts.[0].text}}' by {{response.casts.[0].author.username}} ({{response.casts.[0].reactions.likes}} likes), 2) '{{response.casts.[1].text}}' ({{response.casts.[1].reactions.likes}} likes), 3) '{{response.casts.[2].text}}' ({{response.casts.[2].reactions.likes}} likes)"
277 | )
278 | )
279 |
280 | def _create_get_cast_reactions(self) -> Function:
281 | return Function(
282 | fn_name="get_cast_reactions",
283 | fn_description="Get reactions (likes, recasts) for a specific cast. Use this to gauge a cast's impact.",
284 | args=[
285 | FunctionArgument(
286 | name="cast_hash",
287 | description="Hash of the cast to get reactions for",
288 | type="string"
289 | )
290 | ],
291 | config=FunctionConfig(
292 | method="get",
293 | url=f"{self.base_url}/farcaster/cast/{{cast_hash}}/reactions",
294 | platform="farcaster",
295 | headers=self.base_headers,
296 | success_feedback="Cast has {{response.reactions.likes}} likes and {{response.reactions.recasts}} recasts. Top engaging users: {{response.reactions.top_likers.[0].username}}, {{response.reactions.top_likers.[1].username}}, {{response.reactions.top_likers.[2].username}}"
297 | )
298 | )
299 |
300 | def _create_get_user_casts(self) -> Function:
301 | return Function(
302 | fn_name="get_user_casts",
303 | fn_description="Get recent casts from a specific user. Use this to understand a user's activity and interests.",
304 | args=[
305 | FunctionArgument(
306 | name="fid",
307 | description="Farcaster ID of the user",
308 | type="integer"
309 | )
310 | ],
311 | config=FunctionConfig(
312 | method="get",
313 | url=f"{self.base_url}/farcaster/user/casts",
314 | platform="farcaster",
315 | headers=self.base_headers,
316 | query_params={
317 | "fid": "{{fid}}"
318 | },
319 | success_feedback="Retrieved {{response.casts.length}} casts by {{response.casts.[0].author.username}}. Latest: '{{response.casts.[0].text}}' ({{response.casts.[0].reactions.likes}} likes). Most engaged: '{{response.most_liked_cast.text}}' ({{response.most_liked_cast.reactions.likes}} likes)"
320 | )
321 | )
322 |
323 | def _create_search_casts(self) -> Function:
324 | return Function(
325 | fn_name="search_casts",
326 | fn_description="Search for casts containing specific text or topics.",
327 | args=[
328 | FunctionArgument(
329 | name="query",
330 | description="Text to search for in casts",
331 | type="string"
332 | ),
333 | FunctionArgument(
334 | name="channel_name",
335 | description="Optional: Filter search to a specific channel",
336 | type="string",
337 | required=False
338 | )
339 | ],
340 | config=FunctionConfig(
341 | method="get",
342 | url=f"{self.base_url}/farcaster/cast/search",
343 | platform="farcaster",
344 | headers=self.base_headers,
345 | query_params={
346 | "q": "{{query}}",
347 | "channel": "{{channel_name}}"
348 | },
349 | success_feedback="Found {{response.casts.length}} matching casts. Most relevant: 1) '{{response.casts.[0].text}}' by {{response.casts.[0].author.username}} in channel {{response.casts.[0].channel}} ({{response.casts.[0].reactions.likes}} likes), 2) '{{response.casts.[1].text}}' ({{response.casts.[1].reactions.likes}} likes)"
350 | )
351 | )
352 |
353 | def _create_search_users(self) -> Function:
354 | return Function(
355 | fn_name="search_users",
356 | fn_description="Search for Farcaster users by username or display name.",
357 | args=[
358 | FunctionArgument(
359 | name="query",
360 | description="Text to search for in usernames or display names",
361 | type="string"
362 | )
363 | ],
364 | config=FunctionConfig(
365 | method="get",
366 | url=f"{self.base_url}/farcaster/user/search",
367 | platform="farcaster",
368 | headers=self.base_headers,
369 | query_params={
370 | "q": "{{query}}"
371 | },
372 | success_feedback="Found {{response.users.length}} users. Top matches: {{response.users.[0].username}} ({{response.users.[0].display_name}}), {{response.users.[1].username}} ({{response.users.[1].display_name}})",
373 | error_feedback="Failed to search users: {{response.message}}"
374 | )
375 | )
376 |
377 | def _create_get_user_casts(self) -> Function:
378 | return Function(
379 | fn_name="get_user_casts",
380 | fn_description="Get recent casts from a specific user. Use this to understand a user's activity and interests.",
381 | args=[
382 | FunctionArgument(
383 | name="fid",
384 | description="Farcaster ID of the user",
385 | type="integer"
386 | )
387 | ],
388 | config=FunctionConfig(
389 | method="get",
390 | url=f"{self.base_url}/farcaster/user/casts",
391 | platform="farcaster",
392 | headers=self.base_headers,
393 | query_params={
394 | "fid": "{{fid}}"
395 | },
396 | success_feedback="Retrieved {{response.casts.length}} recent casts. Latest cast: '{{response.casts.[0].text}}' with {{response.casts.[0].reactions.likes}} likes. Most liked cast: '{{response.most_liked_cast.text}}' with {{response.most_liked_cast.reactions.likes}} likes",
397 | error_feedback="Failed to get user's casts: {{response.message}}"
398 | )
399 | )
400 |
401 | def _create_get_cast_reactions(self) -> Function:
402 | return Function(
403 | fn_name="get_cast_reactions",
404 | fn_description="Get reactions (likes, recasts) for a specific cast. Use this to gauge a cast's impact.",
405 | args=[
406 | FunctionArgument(
407 | name="cast_hash",
408 | description="Hash of the cast to get reactions for",
409 | type="string"
410 | )
411 | ],
412 | config=FunctionConfig(
413 | method="get",
414 | url=f"{self.base_url}/farcaster/cast/{{cast_hash}}/reactions",
415 | platform="farcaster",
416 | headers=self.base_headers,
417 | success_feedback="Cast has {{response.reactions.likes}} likes and {{response.reactions.recasts}} recasts. Most engaged users: {{response.reactions.top_likers.[0].username}}, {{response.reactions.top_likers.[1].username}}",
418 | error_feedback="Failed to get cast reactions: {{response.message}}"
419 | )
420 | )
--------------------------------------------------------------------------------
/src/virtuals_sdk/twitter_agent/functions/telegram.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, List
2 | from virtuals_sdk.twitter_agent.game import Function, FunctionConfig, FunctionArgument
3 |
4 | class TelegramClient:
5 | """
6 | A client for managing Telegram bot functions.
7 |
8 | Initialize with your bot token to create Telegram API functions.
9 |
10 | Example:
11 | client = TelegramClient("your-bot-token-here")
12 | send_message = client.get_send_message_function()
13 | """
14 |
15 | def __init__(self, bot_token: str):
16 | """
17 | Initialize the Telegram client with a bot token.
18 |
19 | Args:
20 | bot_token (str): Your Telegram bot token
21 | """
22 | self.bot_token = bot_token
23 |
24 | self._functions: Dict[str, Function] = {
25 | "send_message": self._create_send_message(),
26 | "send_media": self._create_send_media(),
27 | "create_poll": self._create_poll(),
28 | "pin_message": self._create_pin_message(),
29 | "delete_message": self._create_delete_message(),
30 | }
31 |
32 | @property
33 | def available_functions(self) -> List[str]:
34 | """Get list of available function names."""
35 | return list(self._functions.keys())
36 |
37 | def create_api_url(self, endpoint):
38 | """Helper function to create full API URL with token"""
39 | return f"https://api.telegram.org/bot{self.bot_token}/{endpoint}"
40 |
41 | def get_function(self, fn_name: str) -> Function:
42 | """
43 | Get a specific function by name.
44 |
45 | Args:
46 | fn_name: Name of the function to retrieve
47 |
48 | Raises:
49 | ValueError: If function name is not found
50 |
51 | Returns:
52 | Function object
53 | """
54 | if fn_name not in self._functions:
55 | raise ValueError(f"Function '{fn_name}' not found. Available functions: {', '.join(self.available_functions)}")
56 | return self._functions[fn_name]
57 |
58 | def _create_send_message(self) -> Function:
59 |
60 | # Send Message Function
61 | send_message = Function(
62 | fn_name="send_message",
63 | fn_description="Send a text message that is contextually appropriate and adds value to the conversation. Consider chat type (private/group) and ongoing discussion context.",
64 | args=[
65 | FunctionArgument(
66 | name="chat_id",
67 | description="Unique identifier for the target chat or username of the target channel",
68 | type="string"
69 | ),
70 | FunctionArgument(
71 | name="text",
72 | description="Message text to send. Should be contextually relevant and maintain conversation flow.",
73 | type="string"
74 | )
75 | ],
76 | config=FunctionConfig(
77 | method="post",
78 | url=self.create_api_url("sendMessage"),
79 | platform="telegram",
80 | headers={"Content-Type": "application/json"},
81 | payload={
82 | "chat_id": "{{chat_id}}",
83 | "text": "{{text}}",
84 | },
85 | success_feedback="Message sent successfully. Message ID: {{response.result.message_id}}",
86 | error_feedback="Failed to send message: {{response.description}}"
87 | )
88 | )
89 |
90 | return send_message
91 |
92 |
93 | def _create_send_media(self) -> Function:
94 |
95 | # Reply with Media Function
96 | send_media = Function(
97 | fn_name="send_media",
98 | fn_description="Send a media message (photo, document, video, etc.) with optional caption. Use when visual or document content adds value to the conversation.",
99 | args=[
100 | FunctionArgument(
101 | name="chat_id",
102 | description="Target chat identifier where media will be sent",
103 | type="string"
104 | ),
105 | FunctionArgument(
106 | name="media_type",
107 | description="Type of media to send: 'photo', 'document', 'video', 'audio'. Choose appropriate type for content.",
108 | type="string"
109 | ),
110 | FunctionArgument(
111 | name="media",
112 | description="File ID or URL of the media to send. Ensure content is appropriate and relevant.",
113 | type="string"
114 | ),
115 | FunctionArgument(
116 | name="caption",
117 | description="Optional text caption accompanying the media. Should provide context or explanation when needed, or follows up the conversation.",
118 | type="string"
119 | )
120 | ],
121 | config=FunctionConfig(
122 | method="post",
123 | url=self.create_api_url("send{{media_type}}"),
124 | platform="telegram",
125 | headers={"Content-Type": "application/json"},
126 | payload={
127 | "chat_id": "{{chat_id}}",
128 | "{{media_type}}": "{{media}}",
129 | "caption": "{{caption}}"
130 | },
131 | success_feedback="Media sent successfully. Type: {{media_type}}, Message ID: {{response.result.message_id}}",
132 | error_feedback="Failed to send media: {{response.description}}"
133 | )
134 | )
135 |
136 | return send_media
137 |
138 | def _create_poll(self) -> Function:
139 |
140 | # Create Poll Function
141 | create_poll = Function(
142 | fn_name="create_poll",
143 | fn_description="Create an interactive poll to gather user opinions or make group decisions. Useful for engagement and collecting feedback.",
144 | args=[
145 | FunctionArgument(
146 | name="chat_id",
147 | description="Chat where the poll will be created",
148 | type="string"
149 | ),
150 | FunctionArgument(
151 | name="question",
152 | description="Main poll question. Should be clear and specific.",
153 | type="string"
154 | ),
155 | FunctionArgument(
156 | name="options",
157 | description="List of answer options. Make options clear and mutually exclusive.",
158 | type="array"
159 | ),
160 | FunctionArgument(
161 | name="is_anonymous",
162 | description="Whether poll responses are anonymous. Consider privacy and group dynamics.",
163 | type="boolean"
164 | )
165 | ],
166 | config=FunctionConfig(
167 | method="post",
168 | url=self.create_api_url("sendPoll"),
169 | platform="telegram",
170 | headers={"Content-Type": "application/json"},
171 | payload={
172 | "chat_id": "{{chat_id}}",
173 | "question": "{{question}}",
174 | "options": "{{options}}",
175 | "is_anonymous": "{{is_anonymous}}",
176 | },
177 | success_feedback="Poll created successfully. Poll ID: {{response.result.poll.id}}",
178 | error_feedback="Failed to create poll: {{response.description}}"
179 | )
180 | )
181 |
182 | return create_poll
183 |
184 | def _create_pin_message(self) -> Function:
185 |
186 | # Pin Message Function
187 | pin_message = Function(
188 | fn_name="pin_message",
189 | fn_description="Pin an important message in a chat. Use for announcements, important information, or group rules.",
190 | args=[
191 | FunctionArgument(
192 | name="chat_id",
193 | description="Chat where the message will be pinned",
194 | type="string"
195 | ),
196 | FunctionArgument(
197 | name="message_id",
198 | description="ID of the message to pin. Ensure message contains valuable information worth pinning.",
199 | type="string"
200 | ),
201 | FunctionArgument(
202 | name="disable_notification",
203 | description="Whether to send notification about pinned message. Consider group size and message importance.",
204 | type="boolean"
205 | )
206 | ],
207 | config=FunctionConfig(
208 | method="post",
209 | url=self.create_api_url("pinChatMessage"),
210 | platform="telegram",
211 | headers={"Content-Type": "application/json"},
212 | payload={
213 | "chat_id": "{{chat_id}}",
214 | "message_id": "{{message_id}}",
215 | "disable_notification": "{{disable_notification}}"
216 | },
217 | success_feedback="Message pinned successfully",
218 | error_feedback="Failed to pin message: {{response.description}}"
219 | )
220 | )
221 |
222 | return pin_message
223 |
224 | def _create_delete_message(self) -> Function:
225 |
226 | # Delete Message Function
227 | delete_message = Function(
228 | fn_name="delete_message",
229 | fn_description="Delete a message from a chat. Use for moderation or cleaning up outdated information.",
230 | args=[
231 | FunctionArgument(
232 | name="chat_id",
233 | description="Chat containing the message to delete",
234 | type="string"
235 | ),
236 | FunctionArgument(
237 | name="message_id",
238 | description="ID of the message to delete. Consider impact before deletion.",
239 | type="string"
240 | )
241 | ],
242 | config=FunctionConfig(
243 | method="post",
244 | url=self.create_api_url("deleteMessage"),
245 | platform="telegram",
246 | headers={"Content-Type": "application/json"},
247 | payload={
248 | "chat_id": "{{chat_id}}",
249 | "message_id": "{{message_id}}"
250 | },
251 | success_feedback="Message deleted successfully",
252 | error_feedback="Failed to delete message: {{response.description}}"
253 | )
254 | )
255 |
256 | return delete_message
257 |
258 |
259 | ## FAILS BECAUSE CHATS ARE USUALLY PRIVATE AND AGENTS (BOT TOKEN) CANNOT CHANGE PRIVATE CHAT TITLES
260 | # def _create_set_chat_title(self) -> Function:
261 | # # Set Chat Title Function
262 | # set_chat_title = Function(
263 | # fn_name="set_chat_title",
264 | # fn_description="Update the title of a group, supergroup, or channel. Use when title needs updating to reflect current purpose.",
265 | # args=[
266 | # FunctionArgument(
267 | # name="chat_id",
268 | # description="Chat identifier where title will be updated",
269 | # type="string"
270 | # ),
271 | # FunctionArgument(
272 | # name="title",
273 | # description="New chat title. Should be descriptive and appropriate for chat purpose.",
274 | # type="string"
275 | # )
276 | # ],
277 | # config=FunctionConfig(
278 | # method="post",
279 | # url=self.create_api_url("setChatTitle"),
280 | # platform="telegram",
281 | # headers={"Content-Type": "application/json"},
282 | # payload={
283 | # "chat_id": "{{chat_id}}",
284 | # "title": "{{title}}"
285 | # },
286 | # success_feedback="Chat title updated successfully",
287 | # error_feedback="Failed to update chat title: {{response.description}}"
288 | # )
289 | # )
290 |
291 | # return set_chat_title
--------------------------------------------------------------------------------
/src/virtuals_sdk/twitter_agent/sdk.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 |
4 | class GameSDK:
5 | api_url: str = "https://game-api.virtuals.io/api"
6 | api_key: str
7 |
8 | def __init__(self, api_key: str):
9 | self.api_key = api_key
10 |
11 | def functions(self):
12 | """
13 | Get all default functions
14 | """
15 | response = requests.get(
16 | f"{self.api_url}/functions", headers={"x-api-key": self.api_key})
17 |
18 | if (response.status_code != 200):
19 | raise Exception(response.json())
20 |
21 | functions = {}
22 |
23 | for x in response.json()["data"]:
24 | functions[x["fn_name"]] = x["fn_description"]
25 |
26 | return functions
27 |
28 | def simulate(self, session_id: str, goal: str, description: str, world_info: str, functions: list, custom_functions: list):
29 | """
30 | Simulate the agent configuration
31 | """
32 | response = requests.post(
33 | f"{self.api_url}/simulate",
34 | json={
35 | "data": {
36 | "sessionId": session_id,
37 | "goal": goal,
38 | "description": description,
39 | "worldInfo": world_info,
40 | "functions": functions,
41 | "customFunctions": [x.toJson() for x in custom_functions]
42 | }
43 | },
44 | headers={"x-api-key": self.api_key}
45 | )
46 |
47 | if (response.status_code != 200):
48 | raise Exception(response.json())
49 |
50 | return response.json()["data"]
51 |
52 | def react(self, session_id: str, platform: str, goal: str,
53 | description: str, world_info: str, functions: list, custom_functions: list,
54 | event: str = None, task: str = None, tweet_id: str = None):
55 | """
56 | Simulate the agent configuration
57 | """
58 | url = f"{self.api_url}/react/{platform}"
59 |
60 | payload = {
61 | "sessionId": session_id,
62 | "goal": goal,
63 | "description": description,
64 | "worldInfo": world_info,
65 | "functions": functions,
66 | "customFunctions": [x.toJson() for x in custom_functions]
67 | }
68 |
69 | if (event):
70 | payload["event"] = event
71 |
72 | if (task):
73 | payload["task"] = task
74 |
75 | if (tweet_id):
76 | payload["tweetId"] = tweet_id
77 |
78 | print(payload)
79 |
80 | response = requests.post(
81 | url,
82 | json={
83 | "data": payload
84 | },
85 | headers={"x-api-key": self.api_key}
86 | )
87 |
88 | if (response.status_code != 200):
89 | raise Exception(response.json())
90 |
91 | return response.json()["data"]
92 |
93 | def deploy(self, goal: str, description: str, world_info: str, functions: list, custom_functions: list, main_heartbeat: int, reaction_heartbeat: int):
94 | """
95 | Simulate the agent configuration
96 | """
97 | response = requests.post(
98 | f"{self.api_url}/deploy",
99 | json={
100 | "data": {
101 | "goal": goal,
102 | "description": description,
103 | "worldInfo": world_info,
104 | "functions": functions,
105 | "customFunctions": custom_functions,
106 | "gameState" : {
107 | "mainHeartbeat" : main_heartbeat,
108 | "reactionHeartbeat" : reaction_heartbeat,
109 | }
110 | }
111 | },
112 | headers={"x-api-key": self.api_key}
113 | )
114 |
115 | if (response.status_code != 200):
116 | raise Exception(response.json())
117 |
118 | return response.json()["data"]
119 |
--------------------------------------------------------------------------------