Skip to content

Commit

Permalink
another massive refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
saolsen committed May 3, 2023
1 parent d5236e2 commit 7816d47
Show file tree
Hide file tree
Showing 24 changed files with 625 additions and 375 deletions.
1 change: 0 additions & 1 deletion migrations/versions/516ddfaaa55f_timezones.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
Expand Down
60 changes: 60 additions & 0 deletions migrations/versions/ecc49f9f55bc_agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""agents
Revision ID: ecc49f9f55bc
Revises: 516ddfaaa55f
Create Date: 2023-05-02 16:36:47.627765
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "ecc49f9f55bc"
down_revision = "516ddfaaa55f"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"agent_deployment",
sa.Column("agent_id", sa.BigInteger(), nullable=False),
sa.Column("url", sa.String(), nullable=False),
sa.Column("healthy", sa.Boolean(), nullable=False),
sa.Column("active", sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(
["agent_id"],
["agents.id"],
),
sa.PrimaryKeyConstraint("agent_id"),
)
op.create_table(
"agent_history",
sa.Column("agent_id", sa.BigInteger(), nullable=False),
sa.Column("wins", sa.Integer(), nullable=False),
sa.Column("losses", sa.Integer(), nullable=False),
sa.Column("draws", sa.Integer(), nullable=False),
sa.Column("errors", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["agent_id"],
["agents.id"],
),
sa.PrimaryKeyConstraint("agent_id"),
)
op.add_column(
"agents",
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
)
op.execute("UPDATE agents SET created_at = NOW()")
op.alter_column("agents", "created_at", nullable=False)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("agents", "created_at")
op.drop_table("agent_history")
op.drop_table("agent_deployment")
# ### end Alembic commands ###
11 changes: 6 additions & 5 deletions src/gameplay_computer/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
from .schemas import Agent, AgentDeployment
from .repo import (
from .schemas import AgentDeployment, AgentHistory
from .service import (
create_agent,
get_agent_by_id,
get_agent_by_username_and_agentname,
get_agent_id_for_username_and_agentname,
get_agent_deployment_by_id
get_agent_action,
)

__all__ = [
"Agent",
"AgentDeployment",
"AgentHistory",
"get_agent_by_id",
"get_agent_by_username_and_agentname",
"get_agent_id_for_username_and_agentname",
"get_agent_deployment_by_id",
"get_agent_action",
]
65 changes: 54 additions & 11 deletions src/gameplay_computer/agents/repo.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,46 @@
from databases import Database
from .schemas import AgentDeployment, AgentHistory
from . import tables
from gameplay_computer.gameplay import Agent
from gameplay_computer import users
import sqlalchemy

from .schemas import Agent, AgentDeployment
from .tables import agents

from gameplay_computer import users
async def create_agent(
database: Database, created_by_user_id: str, game: str, agentname: str, url: str
) -> int:
async with database.transaction():
agent_id: int = await database.execute(
query=tables.agents.insert().values(
game=game,
user_id=created_by_user_id,
agentname=agentname,
created_at=sqlalchemy.func.now(),
)
)
await database.execute(
query=tables.agent_deployment.insert().values(
agent_id=agent_id,
url=url,
healthy=True,
active=False,
)
)
await database.execute(
query=tables.agent_history.insert().values(
agent_id=agent_id,
wins=0,
losses=0,
draws=0,
errors=0,
)
)
return agent_id


async def get_agent_by_id(database: Database, agent_id: int) -> Agent | None:
agent = await database.fetch_one(
query=agents.select().where(agents.c.id == agent_id)
query=tables.agents.select().where(tables.agents.c.id == agent_id)
)
if agent is None:
return None
Expand All @@ -27,8 +59,9 @@ async def get_agent_by_username_and_agentname(
user_id = await users.get_user_id_for_username(username)
assert user_id is not None
agent = await database.fetch_one(
query=agents.select().where(
(agents.c.user_id == user_id) & (agents.c.agentname == agentname)
query=tables.agents.select().where(
(tables.agents.c.user_id == user_id)
& (tables.agents.c.agentname == agentname)
)
)
if agent is None:
Expand All @@ -46,19 +79,29 @@ async def get_agent_id_for_username_and_agentname(
user_id = await users.get_user_id_for_username(username)
assert user_id is not None
agent_id = await database.fetch_val(
query=agents.select().where(
(agents.c.user_id == user_id) & (agents.c.agentname == agentname)
query=tables.agents.select().where(
(tables.agents.c.user_id == user_id)
& (tables.agents.c.agentname == agentname)
),
column="id",
)
if agent_id is None:
return None
return int(agent_id)


async def get_agent_deployment_by_id(
database: Database, agent_id: int
) -> AgentDeployment:
) -> AgentDeployment | None:
agent_deployment = await database.fetch_one(
query=tables.agent_deployment.select().where(
tables.agent_deployment.c.agent_id == agent_id
)
)
if agent_deployment is None:
return None
return AgentDeployment(
url="https://gameplay-agents.fly.dev/connect4_random",
active=True,
url=agent_deployment["url"],
healthy=agent_deployment["healthy"],
active=agent_deployment["active"],
)
16 changes: 8 additions & 8 deletions src/gameplay_computer/agents/schemas.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from typing import Literal
from pydantic import BaseModel, HttpUrl

from gameplay_computer.common import BasePlayer, Game


class Agent(BasePlayer):
kind: Literal["agent"] = "agent"
game: Game
username: str
agentname: str

class AgentDeployment(BaseModel):
url: HttpUrl
active: bool
healthy: bool


class AgentHistory(BaseModel):
wins: int
losses: int
draws: int
errors: int
112 changes: 112 additions & 0 deletions src/gameplay_computer/agents/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from databases import Database
from fastapi import HTTPException, status
import httpx
import asyncio

from gameplay_computer.gameplay import Match, Connect4Action, Action, Game, Agent

from gameplay_computer import users
from . import repo


async def create_agent(
database: Database,
created_by_user_id: str,
game: Game,
agentname: str,
url: str,
) -> int:
created_by_user = await users.get_user_by_id(created_by_user_id)
if created_by_user is None:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Unknown user.",
)
agent_id = await repo.create_agent(
database, created_by_user_id, game, agentname, url
)
return agent_id


async def get_agent_by_id(database: Database, agent_id: int) -> Agent:
agent = await repo.get_agent_by_id(database, agent_id)
if agent is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Unknown agent.",
)
return agent


async def get_agent_by_username_and_agentname(
database: Database, username: str, agentname: str
) -> Agent:
agent = await repo.get_agent_by_username_and_agentname(
database, username, agentname
)
if agent is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Unknown agent.",
)
return agent


async def get_agent_id_for_username_and_agentname(
database: Database, username: str, agentname: str
) -> int:
agent_id = await repo.get_agent_id_for_username_and_agentname(
database, username, agentname
)
if agent_id is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Unknown agent.",
)
return agent_id


async def get_agent_action(
database: Database,
client: httpx.AsyncClient,
agent_id: int,
match: Match,
) -> Action:
agent = await get_agent_by_id(database, agent_id)
deployment = await repo.get_agent_deployment_by_id(database, agent_id)
if deployment is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Unknown agent.",
)
if agent.game != match.game:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Wrong game.",
)
# We are sort of assuming right now that we know the agent is the next player and that
# somebody else checked that before calling this. tbd

action: Connect4Action | None = None

retries = 0
while retries < 3:
try:
response = await client.post(deployment.url, json=match.dict())
response.raise_for_status()
action = Connect4Action(**response.json())
break
except httpx.HTTPError as e:
print(f"Error: {e}")
retries += 1
await asyncio.sleep(retries)

# todo: log errors
if action is None:
print("ERROR: too many agent errors")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Too many agent errors.",
)

return action
30 changes: 30 additions & 0 deletions src/gameplay_computer/agents/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,35 @@
sqlalchemy.Column("game", game, nullable=False, index=True),
sqlalchemy.Column("user_id", sqlalchemy.String, nullable=False, index=True),
sqlalchemy.Column("agentname", sqlalchemy.String, nullable=False),
sqlalchemy.Column("created_at", sqlalchemy.DateTime(timezone=True), nullable=False),
sqlalchemy.UniqueConstraint("game", "user_id", "agentname"),
)

agent_deployment = sqlalchemy.Table(
"agent_deployment",
metadata,
sqlalchemy.Column(
"agent_id",
sqlalchemy.BigInteger,
sqlalchemy.ForeignKey("agents.id"),
primary_key=True,
),
sqlalchemy.Column("url", sqlalchemy.String, nullable=False),
sqlalchemy.Column("healthy", sqlalchemy.Boolean, nullable=False),
sqlalchemy.Column("active", sqlalchemy.Boolean, nullable=False),
)

agent_history = sqlalchemy.Table(
"agent_history",
metadata,
sqlalchemy.Column(
"agent_id",
sqlalchemy.BigInteger,
sqlalchemy.ForeignKey("agents.id"),
primary_key=True,
),
sqlalchemy.Column("wins", sqlalchemy.Integer, nullable=False),
sqlalchemy.Column("losses", sqlalchemy.Integer, nullable=False),
sqlalchemy.Column("draws", sqlalchemy.Integer, nullable=False),
sqlalchemy.Column("errors", sqlalchemy.Integer, nullable=False),
)
17 changes: 12 additions & 5 deletions src/gameplay_computer/common/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from . import tables
from .schemas import Game, BasePlayer, BaseAction, BaseState
from .schemas import ALogic
from .service import (
serialize_state,
serialize_action,
deserialize_state,
deserialize_action,
)

__all__ = [
"tables",
"Game",
"BasePlayer",
"BaseAction",
"BaseState",
"ALogic",
"serialize_state",
"serialize_action",
"deserialize_state",
"deserialize_action",
]
Loading

0 comments on commit 7816d47

Please sign in to comment.