Skip to content

Commit d69fd89

Browse files
authored
feat: add agent operations and generate config support (#49)
* feat: add agents tables and migrations * feat: add agents crud routes and openapi * fix: auth logic * feat: add yaml generating logic * feat: use postgres db instead of sqlite * fix: add deployment_id in agents model * chore: add more agent status * feat: add status transition logics
1 parent b03a328 commit d69fd89

31 files changed

+3983
-51
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,7 @@ cython_debug/
280280
.idea/
281281

282282
examples/storage/
283+
284+
storage/
285+
286+
agent_deployments/

alembic.ini

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# A generic, single database configuration.
2+
3+
[alembic]
4+
# path to migration scripts
5+
# Use forward slashes (/) also on windows to provide an os agnostic path
6+
script_location = migrations
7+
8+
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
9+
# Uncomment the line below if you want the files to be prepended with date and time
10+
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
11+
# for all available tokens
12+
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
13+
14+
# sys.path path, will be prepended to sys.path if present.
15+
# defaults to the current working directory.
16+
prepend_sys_path = .
17+
18+
# timezone to use when rendering the date within the migration file
19+
# as well as the filename.
20+
# If specified, requires the python>=3.9 or backports.zoneinfo library.
21+
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
22+
# string value is passed to ZoneInfo()
23+
# leave blank for localtime
24+
# timezone =
25+
26+
# max length of characters to apply to the "slug" field
27+
# truncate_slug_length = 40
28+
29+
# set to 'true' to run the environment during
30+
# the 'revision' command, regardless of autogenerate
31+
# revision_environment = false
32+
33+
# set to 'true' to allow .pyc and .pyo files without
34+
# a source .py file to be detected as revisions in the
35+
# versions/ directory
36+
# sourceless = false
37+
38+
# version location specification; This defaults
39+
# to migrations/versions. When using multiple version
40+
# directories, initial revisions must be specified with --version-path.
41+
# The path separator used here should be the separator specified by "version_path_separator" below.
42+
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
43+
44+
# version path separator; As mentioned above, this is the character used to split
45+
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
46+
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
47+
# Valid values for version_path_separator are:
48+
#
49+
# version_path_separator = :
50+
# version_path_separator = ;
51+
# version_path_separator = space
52+
# version_path_separator = newline
53+
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
54+
55+
# set to 'true' to search source files recursively
56+
# in each "version_locations" directory
57+
# new in Alembic version 1.10
58+
# recursive_version_locations = false
59+
60+
# the output encoding used when revision files
61+
# are written from script.py.mako
62+
# output_encoding = utf-8
63+
64+
sqlalchemy.url = postgresql://postgres:password@localhost:5432/openagent
65+
66+
67+
[post_write_hooks]
68+
# post_write_hooks defines scripts or Python functions that are run
69+
# on newly generated revision scripts. See the documentation for further
70+
# detail and examples
71+
72+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
73+
# hooks = black
74+
# black.type = console_scripts
75+
# black.entrypoint = black
76+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
77+
78+
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
79+
# hooks = ruff
80+
# ruff.type = exec
81+
# ruff.executable = %(here)s/.venv/bin/ruff
82+
# ruff.options = --fix REVISION_SCRIPT_FILENAME
83+
84+
# Logging configuration
85+
[loggers]
86+
keys = root,sqlalchemy,alembic
87+
88+
[handlers]
89+
keys = console
90+
91+
[formatters]
92+
keys = generic
93+
94+
[logger_root]
95+
level = WARNING
96+
handlers = console
97+
qualname =
98+
99+
[logger_sqlalchemy]
100+
level = WARNING
101+
handlers =
102+
qualname = sqlalchemy.engine
103+
104+
[logger_alembic]
105+
level = INFO
106+
handlers =
107+
qualname = alembic
108+
109+
[handler_console]
110+
class = StreamHandler
111+
args = (sys.stderr,)
112+
level = NOTSET
113+
formatter = generic
114+
115+
[formatter_generic]
116+
format = %(levelname)-5.5s [%(name)s] %(message)s
117+
datefmt = %H:%M:%S

docker-compose.db.yml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
version: "3"
2+
services:
3+
postgres:
4+
image: postgres:latest
5+
container_name: openagent-postgres
6+
restart: always
7+
ports:
8+
- 5432:5432
9+
environment:
10+
- POSTGRES_USER=postgres
11+
- POSTGRES_PASSWORD=password
12+
- POSTGRES_DB=openagent
13+
volumes:
14+
- postgres:/var/lib/postgresql/data
15+
16+
volumes:
17+
postgres:
18+
name: openagent-postgres

migrations/env.py

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import os
2+
from logging.config import fileConfig
3+
4+
from alembic import context
5+
from dotenv import load_dotenv
6+
from sqlalchemy import engine_from_config, pool
7+
8+
from openagent.database.models.base import Base
9+
10+
load_dotenv()
11+
12+
# this is the Alembic Config object, which provides
13+
# access to the values within the .ini file in use.
14+
config = context.config
15+
16+
# Get the database URL from config, which can be overridden during runtime
17+
# This allows DatabaseManager to set the URL for SQLite
18+
db_url = config.get_main_option("sqlalchemy.url")
19+
20+
# Use environment variable only as fallback
21+
if not db_url:
22+
db_url = os.getenv("DATABASE_URL")
23+
if db_url is None:
24+
raise ValueError(
25+
"No sqlalchemy.url in alembic.ini and no DATABASE_URL set in environment"
26+
)
27+
28+
# Set the URL in config if it came from environment
29+
config.set_main_option("sqlalchemy.url", db_url)
30+
31+
# Interpret the config file for Python logging.
32+
# This line sets up loggers basically.
33+
if config.config_file_name is not None:
34+
fileConfig(config.config_file_name)
35+
36+
# add your model's MetaData object here
37+
# for 'autogenerate' support
38+
# from myapp import mymodel
39+
# target_metadata = mymodel.Base.metadata
40+
target_metadata = Base.metadata
41+
42+
# other values from the config, defined by the needs of env.py,
43+
# can be acquired:
44+
# my_important_option = config.get_main_option("my_important_option")
45+
# ... etc.
46+
47+
48+
def run_migrations_offline() -> None:
49+
"""Run migrations in 'offline' mode.
50+
51+
This configures the context with just a URL
52+
and not an Engine, though an Engine is acceptable
53+
here as well. By skipping the Engine creation
54+
we don't even need a DBAPI to be available.
55+
56+
Calls to context.execute() here emit the given string to the
57+
script output.
58+
59+
"""
60+
url = config.get_main_option("sqlalchemy.url")
61+
62+
context.configure(
63+
url=url,
64+
target_metadata=target_metadata,
65+
literal_binds=True,
66+
dialect_opts={"paramstyle": "named"},
67+
)
68+
69+
with context.begin_transaction():
70+
context.run_migrations()
71+
72+
73+
def run_migrations_online() -> None:
74+
"""Run migrations in 'online' mode.
75+
76+
In this scenario we need to create an Engine
77+
and associate a connection with the context.
78+
79+
"""
80+
connectable = engine_from_config(
81+
config.get_section(config.config_ini_section),
82+
prefix="sqlalchemy.",
83+
poolclass=pool.NullPool,
84+
)
85+
86+
with connectable.connect() as connection:
87+
# Determine if we're using SQLite
88+
is_sqlite = "sqlite" in str(connection.engine.url)
89+
90+
# Configure with SQLite-specific settings if needed
91+
context.configure(
92+
connection=connection,
93+
target_metadata=target_metadata,
94+
# For SQLite, we need to disable constraint usage for some operations
95+
render_as_batch=is_sqlite,
96+
)
97+
98+
with context.begin_transaction():
99+
context.run_migrations()
100+
101+
102+
if context.is_offline_mode():
103+
run_migrations_offline()
104+
else:
105+
run_migrations_online()

migrations/script.py.mako

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""${message}
2+
3+
Revision ID: ${up_revision}
4+
Revises: ${down_revision | comma,n}
5+
Create Date: ${create_date}
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
${imports if imports else ""}
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = ${repr(up_revision)}
16+
down_revision: Union[str, None] = ${repr(down_revision)}
17+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19+
20+
21+
def upgrade() -> None:
22+
# Check if we're using SQLite (use _variable to indicate intentionally unused)
23+
_is_sqlite = op.get_context().dialect.name == 'sqlite'
24+
25+
# SQLite-specific workarounds can be added here if needed
26+
27+
${upgrades if upgrades else "pass"}
28+
29+
30+
def downgrade() -> None:
31+
# Check if we're using SQLite (use _variable to indicate intentionally unused)
32+
_is_sqlite = op.get_context().dialect.name == 'sqlite'
33+
34+
# SQLite-specific workarounds can be added here if needed
35+
36+
${downgrades if downgrades else "pass"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""create initial tables
2+
3+
Revision ID: 6e5fcfc0cb29
4+
Revises:
5+
Create Date: 2025-02-27 16:37:55.184916
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
from alembic import op
12+
import sqlalchemy as sa
13+
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "6e5fcfc0cb29"
17+
down_revision: Union[str, None] = None
18+
branch_labels: Union[str, Sequence[str], None] = None
19+
depends_on: Union[str, Sequence[str], None] = None
20+
21+
22+
def upgrade() -> None:
23+
# Check if we're using SQLite - prefix with _ to indicate intentionally unused
24+
_is_sqlite = op.get_context().dialect.name == "sqlite"
25+
26+
# SQLite-specific workarounds can be added here if needed
27+
28+
# Create models table
29+
op.create_table(
30+
"models",
31+
sa.Column("id", sa.Integer(), primary_key=True),
32+
sa.Column("name", sa.String(), nullable=False, unique=True),
33+
sa.Column("description", sa.Text()),
34+
sa.Column("capability_score", sa.Float(), nullable=False),
35+
sa.Column("capabilities", sa.String()),
36+
sa.Column(
37+
"created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()
38+
),
39+
sa.Column(
40+
"updated_at", sa.DateTime(), nullable=False, server_default=sa.func.now()
41+
),
42+
)
43+
44+
# Create tools table
45+
op.create_table(
46+
"tools",
47+
sa.Column("id", sa.Integer(), primary_key=True),
48+
sa.Column("name", sa.String(), nullable=False),
49+
sa.Column("description", sa.Text()),
50+
sa.Column("type", sa.String(), nullable=False),
51+
sa.Column(
52+
"created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()
53+
),
54+
sa.Column(
55+
"updated_at", sa.DateTime(), nullable=False, server_default=sa.func.now()
56+
),
57+
)
58+
59+
# Create agents table
60+
op.create_table(
61+
"agents",
62+
sa.Column("id", sa.Integer(), primary_key=True),
63+
sa.Column("deployment_id", sa.String(), nullable=True, unique=True),
64+
sa.Column("name", sa.String(), nullable=False, unique=True),
65+
sa.Column("description", sa.String()),
66+
sa.Column("personality", sa.String()),
67+
sa.Column("instruction", sa.String()),
68+
sa.Column("wallet_address", sa.String(), nullable=False),
69+
sa.Column("token_image", sa.String()),
70+
sa.Column("ticker", sa.String(), nullable=False),
71+
sa.Column("contract_address", sa.String()),
72+
sa.Column("pair_address", sa.String()),
73+
sa.Column("twitter", sa.String()),
74+
sa.Column("telegram", sa.String()),
75+
sa.Column("website", sa.String()),
76+
sa.Column("tool_configs", sa.JSON()), # Using native JSON type for PostgreSQL
77+
sa.Column("status", sa.String(), nullable=False),
78+
sa.Column("type", sa.String(), nullable=False),
79+
sa.Column(
80+
"created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()
81+
),
82+
sa.Column(
83+
"updated_at", sa.DateTime(), nullable=False, server_default=sa.func.now()
84+
),
85+
)
86+
87+
88+
def downgrade() -> None:
89+
# Check if we're using SQLite - prefix with _ to indicate intentionally unused
90+
_is_sqlite = op.get_context().dialect.name == "sqlite"
91+
92+
# SQLite-specific workarounds can be added here if needed
93+
94+
# Drop all tables
95+
op.drop_table("agents")
96+
op.drop_table("tools")
97+
op.drop_table("models")

0 commit comments

Comments
 (0)