NOT-OMEGA commited on
Commit
82b3d5a
·
verified ·
1 Parent(s): 6a0ff33

Delete backend

Browse files
backend/__init__.py DELETED
@@ -1 +0,0 @@
1
- """AutoForge backend package."""
 
 
backend/__pycache__/__init__.cpython-312.pyc DELETED
Binary file (199 Bytes)
 
backend/agents/__init__.py DELETED
@@ -1 +0,0 @@
1
- """AutoForge agents package."""
 
 
backend/analysis/__init__.py DELETED
@@ -1 +0,0 @@
1
- """AutoForge analysis package."""
 
 
backend/api/__init__.py DELETED
@@ -1 +0,0 @@
1
- """AutoForge API package."""
 
 
backend/api/auth.py DELETED
@@ -1,110 +0,0 @@
1
- """Authentication API: signup, login, and current-user retrieval."""
2
-
3
- from datetime import datetime, timedelta, timezone
4
-
5
- from fastapi import APIRouter, Depends, HTTPException, status
6
- from jose import jwt
7
- from passlib.context import CryptContext
8
- from pydantic import BaseModel, EmailStr, Field
9
- from sqlalchemy.ext.asyncio import AsyncSession
10
-
11
- from backend.api.deps import get_current_user, get_db
12
- from backend.config.settings import get_settings
13
- from backend.db.models import User
14
- from backend.db.repository import create_user, get_user_by_email, get_user_by_username
15
-
16
- router = APIRouter(prefix="/api/auth", tags=["auth"])
17
-
18
- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
19
-
20
-
21
- # ── Schemas ──────────────────────────────────────────────────────────
22
-
23
- class SignupRequest(BaseModel):
24
- username: str = Field(..., min_length=3, max_length=80)
25
- email: EmailStr
26
- password: str = Field(..., min_length=6)
27
-
28
-
29
- class LoginRequest(BaseModel):
30
- username: str
31
- password: str
32
-
33
-
34
- class TokenResponse(BaseModel):
35
- access_token: str
36
- token_type: str = "bearer"
37
-
38
-
39
- class UserResponse(BaseModel):
40
- id: str
41
- username: str
42
- email: str
43
- created_at: datetime
44
-
45
- model_config = {"from_attributes": True}
46
-
47
-
48
- # ── Helpers ──────────────────────────────────────────────────────────
49
-
50
- def _hash_password(password: str) -> str:
51
- return pwd_context.hash(password)
52
-
53
-
54
- def _verify_password(plain: str, hashed: str) -> bool:
55
- return pwd_context.verify(plain, hashed)
56
-
57
-
58
- def _create_token(user_id: str) -> str:
59
- settings = get_settings()
60
- expire = datetime.now(timezone.utc) + timedelta(minutes=settings.jwt_expire_minutes)
61
- payload = {"sub": user_id, "exp": expire}
62
- return jwt.encode(payload, settings.secret_key, algorithm=settings.jwt_algorithm)
63
-
64
-
65
- # ── Routes ───────────────────────────────────────────────────────────
66
-
67
- @router.post("/signup", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
68
- async def signup(body: SignupRequest, session: AsyncSession = Depends(get_db)):
69
- """Register a new user account."""
70
- # Check for duplicate username
71
- if await get_user_by_username(session, body.username):
72
- raise HTTPException(
73
- status_code=status.HTTP_409_CONFLICT,
74
- detail="Username already taken",
75
- )
76
-
77
- # Check for duplicate email
78
- if await get_user_by_email(session, body.email):
79
- raise HTTPException(
80
- status_code=status.HTTP_409_CONFLICT,
81
- detail="Email already registered",
82
- )
83
-
84
- user = await create_user(
85
- session,
86
- username=body.username,
87
- email=body.email,
88
- hashed_password=_hash_password(body.password),
89
- )
90
-
91
- return TokenResponse(access_token=_create_token(user.id))
92
-
93
-
94
- @router.post("/login", response_model=TokenResponse)
95
- async def login(body: LoginRequest, session: AsyncSession = Depends(get_db)):
96
- """Authenticate and receive a JWT."""
97
- user = await get_user_by_username(session, body.username)
98
- if not user or not _verify_password(body.password, user.hashed_password):
99
- raise HTTPException(
100
- status_code=status.HTTP_401_UNAUTHORIZED,
101
- detail="Invalid username or password",
102
- )
103
-
104
- return TokenResponse(access_token=_create_token(user.id))
105
-
106
-
107
- @router.get("/me", response_model=UserResponse)
108
- async def me(current_user: User = Depends(get_current_user)):
109
- """Return the currently authenticated user."""
110
- return current_user
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/api/dependencies.py DELETED
@@ -1,49 +0,0 @@
1
- """
2
- Authentication dependencies for FastAPI.
3
-
4
- Extracts and validates JWT from the Authorization header.
5
- """
6
-
7
- from fastapi import Depends, HTTPException, status
8
- from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
9
- from jose import JWTError, jwt
10
- from sqlalchemy.orm import Session
11
-
12
- from backend.config.settings import get_settings
13
- from backend.db.database import get_db
14
- from backend.db.models import User
15
-
16
- security = HTTPBearer()
17
-
18
-
19
- def get_current_user(
20
- credentials: HTTPAuthorizationCredentials = Depends(security),
21
- db: Session = Depends(get_db),
22
- ) -> User:
23
- """Decode the JWT and return the authenticated User, or raise 401."""
24
- settings = get_settings()
25
- token = credentials.credentials
26
-
27
- credentials_exception = HTTPException(
28
- status_code=status.HTTP_401_UNAUTHORIZED,
29
- detail="Invalid or expired token",
30
- headers={"WWW-Authenticate": "Bearer"},
31
- )
32
-
33
- try:
34
- payload = jwt.decode(
35
- token,
36
- settings.SECRET_KEY,
37
- algorithms=[settings.JWT_ALGORITHM],
38
- )
39
- user_id: int | None = payload.get("sub")
40
- if user_id is None:
41
- raise credentials_exception
42
- except JWTError:
43
- raise credentials_exception
44
-
45
- user = db.query(User).filter(User.id == int(user_id)).first()
46
- if user is None:
47
- raise credentials_exception
48
-
49
- return user
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/api/deps.py DELETED
@@ -1,46 +0,0 @@
1
- """Shared FastAPI dependencies."""
2
-
3
- from fastapi import Depends, HTTPException, status
4
- from fastapi.security import OAuth2PasswordBearer
5
- from jose import JWTError, jwt
6
- from sqlalchemy.ext.asyncio import AsyncSession
7
-
8
- from backend.config.settings import get_settings
9
- from backend.db.database import async_session_factory
10
- from backend.db.repository import get_user_by_id
11
-
12
- oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
13
-
14
-
15
- async def get_db() -> AsyncSession:
16
- """Yield an async database session for request scope."""
17
- async with async_session_factory() as session:
18
- yield session
19
-
20
-
21
- async def get_current_user(
22
- token: str = Depends(oauth2_scheme),
23
- session: AsyncSession = Depends(get_db),
24
- ):
25
- """Decode JWT and return the authenticated user."""
26
- settings = get_settings()
27
- credentials_exception = HTTPException(
28
- status_code=status.HTTP_401_UNAUTHORIZED,
29
- detail="Invalid or expired token",
30
- headers={"WWW-Authenticate": "Bearer"},
31
- )
32
-
33
- try:
34
- payload = jwt.decode(
35
- token, settings.secret_key, algorithms=[settings.jwt_algorithm]
36
- )
37
- user_id: str = payload.get("sub")
38
- if user_id is None:
39
- raise credentials_exception
40
- except JWTError:
41
- raise credentials_exception
42
-
43
- user = await get_user_by_id(session, user_id)
44
- if user is None:
45
- raise credentials_exception
46
- return user
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/api/health.py DELETED
@@ -1,17 +0,0 @@
1
- """Health check endpoint."""
2
-
3
- from fastapi import APIRouter
4
- from pydantic import BaseModel
5
-
6
- router = APIRouter(tags=["health"])
7
-
8
-
9
- class HealthResponse(BaseModel):
10
- status: str
11
- version: str
12
-
13
-
14
- @router.get("/health", response_model=HealthResponse)
15
- async def health():
16
- """Return application health status."""
17
- return HealthResponse(status="ok", version="0.1.0")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/api/workflows.py DELETED
@@ -1,81 +0,0 @@
1
- """Workflow API endpoints."""
2
-
3
- from datetime import datetime
4
- from typing import Optional
5
-
6
- from fastapi import APIRouter, Depends, HTTPException, status
7
- from pydantic import BaseModel, Field
8
- from sqlalchemy.ext.asyncio import AsyncSession
9
-
10
- from backend.api.deps import get_current_user, get_db
11
- from backend.db.models import User
12
- from backend.db.repository import create_workflow, get_workflow_by_id, get_workflows_by_user
13
-
14
- router = APIRouter(prefix="/api/workflows", tags=["workflows"])
15
-
16
-
17
- # ── Schemas ──────────────────────────────────────────────────────────
18
-
19
- class WorkflowCreateRequest(BaseModel):
20
- prompt: str = Field(..., min_length=1)
21
-
22
-
23
- class TaskResponse(BaseModel):
24
- id: str
25
- task_type: str
26
- status: str
27
- created_at: datetime
28
-
29
- model_config = {"from_attributes": True}
30
-
31
-
32
- class WorkflowResponse(BaseModel):
33
- id: str
34
- prompt: str
35
- status: str
36
- created_at: datetime
37
- updated_at: datetime
38
-
39
- model_config = {"from_attributes": True}
40
-
41
-
42
- class WorkflowDetailResponse(WorkflowResponse):
43
- tasks: list[TaskResponse] = []
44
-
45
-
46
- # ── Routes ───────────────────────────────────────────────────────────
47
-
48
- @router.post("", response_model=WorkflowResponse, status_code=status.HTTP_201_CREATED)
49
- async def create(
50
- body: WorkflowCreateRequest,
51
- current_user: User = Depends(get_current_user),
52
- session: AsyncSession = Depends(get_db),
53
- ):
54
- """Create a new workflow (status=pending, no execution yet)."""
55
- workflow = await create_workflow(session, user_id=current_user.id, prompt=body.prompt)
56
- return workflow
57
-
58
-
59
- @router.get("", response_model=list[WorkflowResponse])
60
- async def list_workflows(
61
- current_user: User = Depends(get_current_user),
62
- session: AsyncSession = Depends(get_db),
63
- ):
64
- """List all workflows for the current user."""
65
- workflows = await get_workflows_by_user(session, current_user.id)
66
- return workflows
67
-
68
-
69
- @router.get("/{workflow_id}", response_model=WorkflowDetailResponse)
70
- async def get_workflow(
71
- workflow_id: str,
72
- current_user: User = Depends(get_current_user),
73
- session: AsyncSession = Depends(get_db),
74
- ):
75
- """Get a single workflow with its tasks."""
76
- workflow = await get_workflow_by_id(session, workflow_id)
77
- if not workflow:
78
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workflow not found")
79
- if workflow.user_id != current_user.id:
80
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
81
- return workflow
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/config/__init__.py DELETED
@@ -1 +0,0 @@
1
- """AutoForge configuration package."""
 
 
backend/config/__pycache__/__init__.cpython-312.pyc DELETED
Binary file (212 Bytes)
 
backend/config/__pycache__/settings.cpython-312.pyc DELETED
Binary file (1.64 kB)
 
backend/config/settings.py DELETED
@@ -1,39 +0,0 @@
1
- """Application settings loaded from environment variables."""
2
-
3
- from pydantic_settings import BaseSettings
4
- from pydantic import Field
5
-
6
-
7
- class Settings(BaseSettings):
8
- """AutoForge configuration."""
9
-
10
- # Application
11
- secret_key: str = Field(default="change-this-to-a-random-secret-key")
12
- debug: bool = Field(default=True)
13
- host: str = Field(default="0.0.0.0")
14
- port: int = Field(default=8000)
15
-
16
- # Database
17
- database_url: str = Field(default="sqlite+aiosqlite:///data/autoforge.db")
18
-
19
- # Gemini LLM
20
- gemini_api_key: str = Field(default="")
21
- gemini_model: str = Field(default="gemini-2.0-flash")
22
-
23
- # Docker
24
- docker_enabled: bool = Field(default=False)
25
-
26
- # JWT
27
- jwt_algorithm: str = Field(default="HS256")
28
- jwt_expire_minutes: int = Field(default=1440) # 24 hours
29
-
30
- model_config = {
31
- "env_file": ".env",
32
- "env_file_encoding": "utf-8",
33
- "case_sensitive": False,
34
- }
35
-
36
-
37
- def get_settings() -> Settings:
38
- """Return application settings singleton."""
39
- return Settings()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/db/__init__.py DELETED
@@ -1 +0,0 @@
1
- """AutoForge database package."""
 
 
backend/db/__pycache__/__init__.cpython-312.pyc DELETED
Binary file (203 Bytes)
 
backend/db/__pycache__/database.cpython-312.pyc DELETED
Binary file (2.06 kB)
 
backend/db/database.py DELETED
@@ -1,41 +0,0 @@
1
- """SQLAlchemy async database engine and session management."""
2
-
3
- from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
4
- from sqlalchemy.orm import DeclarativeBase
5
-
6
- from backend.config.settings import get_settings
7
-
8
-
9
- class Base(DeclarativeBase):
10
- """SQLAlchemy declarative base for all models."""
11
- pass
12
-
13
-
14
- _settings = get_settings()
15
-
16
- engine = create_async_engine(
17
- _settings.database_url,
18
- echo=_settings.debug,
19
- connect_args={"check_same_thread": False},
20
- )
21
-
22
- async_session_factory = async_sessionmaker(
23
- engine,
24
- class_=AsyncSession,
25
- expire_on_commit=False,
26
- )
27
-
28
-
29
- async def init_db() -> None:
30
- """Create all database tables."""
31
- # Import models so they register with Base.metadata
32
- import backend.db.models # noqa: F401
33
-
34
- async with engine.begin() as conn:
35
- await conn.run_sync(Base.metadata.create_all)
36
-
37
-
38
- async def get_async_session() -> AsyncSession:
39
- """Yield an async database session."""
40
- async with async_session_factory() as session:
41
- yield session
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/db/models.py DELETED
@@ -1,109 +0,0 @@
1
- """SQLAlchemy ORM models for AutoForge."""
2
-
3
- import uuid
4
- from datetime import datetime, timezone
5
-
6
- from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text
7
- from sqlalchemy.orm import relationship
8
-
9
- from backend.db.database import Base
10
-
11
-
12
- def _utcnow() -> datetime:
13
- return datetime.now(timezone.utc)
14
-
15
-
16
- def _generate_uuid() -> str:
17
- return str(uuid.uuid4())
18
-
19
-
20
- class User(Base):
21
- """User account."""
22
-
23
- __tablename__ = "users"
24
-
25
- id = Column(String(36), primary_key=True, default=_generate_uuid)
26
- username = Column(String(80), unique=True, nullable=False, index=True)
27
- email = Column(String(255), unique=True, nullable=False, index=True)
28
- hashed_password = Column(String(255), nullable=False)
29
- created_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
30
-
31
- workflows = relationship("Workflow", back_populates="user", lazy="selectin")
32
-
33
- def __repr__(self) -> str:
34
- return f"<User {self.username}>"
35
-
36
-
37
- class Workflow(Base):
38
- """A code-generation workflow initiated by a user."""
39
-
40
- __tablename__ = "workflows"
41
-
42
- id = Column(String(36), primary_key=True, default=_generate_uuid)
43
- user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True)
44
- prompt = Column(Text, nullable=False)
45
- status = Column(String(20), nullable=False, default="pending")
46
- created_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
47
- updated_at = Column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow, nullable=False)
48
-
49
- user = relationship("User", back_populates="workflows")
50
- tasks = relationship("Task", back_populates="workflow", lazy="selectin")
51
- events = relationship("Event", back_populates="workflow", lazy="selectin")
52
-
53
- def __repr__(self) -> str:
54
- return f"<Workflow {self.id} [{self.status}]>"
55
-
56
-
57
- class Task(Base):
58
- """A single task within a workflow DAG."""
59
-
60
- __tablename__ = "tasks"
61
-
62
- id = Column(String(36), primary_key=True, default=_generate_uuid)
63
- workflow_id = Column(String(36), ForeignKey("workflows.id"), nullable=False, index=True)
64
- task_type = Column(String(50), nullable=False)
65
- status = Column(String(20), nullable=False, default="pending")
66
- input_data = Column(Text, nullable=True)
67
- output_data = Column(Text, nullable=True)
68
- created_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
69
-
70
- workflow = relationship("Workflow", back_populates="tasks")
71
- attempts = relationship("Attempt", back_populates="task", lazy="selectin")
72
-
73
- def __repr__(self) -> str:
74
- return f"<Task {self.id} [{self.task_type}]>"
75
-
76
-
77
- class Attempt(Base):
78
- """A single execution attempt for a task (supports retry logic)."""
79
-
80
- __tablename__ = "attempts"
81
-
82
- id = Column(String(36), primary_key=True, default=_generate_uuid)
83
- task_id = Column(String(36), ForeignKey("tasks.id"), nullable=False, index=True)
84
- attempt_number = Column(Integer, nullable=False, default=1)
85
- status = Column(String(20), nullable=False, default="pending")
86
- error_message = Column(Text, nullable=True)
87
- created_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
88
-
89
- task = relationship("Task", back_populates="attempts")
90
-
91
- def __repr__(self) -> str:
92
- return f"<Attempt {self.id} #{self.attempt_number}>"
93
-
94
-
95
- class Event(Base):
96
- """A workflow event for streaming to the frontend."""
97
-
98
- __tablename__ = "events"
99
-
100
- id = Column(String(36), primary_key=True, default=_generate_uuid)
101
- workflow_id = Column(String(36), ForeignKey("workflows.id"), nullable=False, index=True)
102
- event_type = Column(String(50), nullable=False)
103
- payload = Column(Text, nullable=True)
104
- created_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
105
-
106
- workflow = relationship("Workflow", back_populates="events")
107
-
108
- def __repr__(self) -> str:
109
- return f"<Event {self.id} [{self.event_type}]>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/db/repository.py DELETED
@@ -1,124 +0,0 @@
1
- """Repository layer for database CRUD operations."""
2
-
3
- from typing import Optional
4
-
5
- from sqlalchemy import select
6
- from sqlalchemy.ext.asyncio import AsyncSession
7
-
8
- from backend.db.models import Attempt, Event, Task, User, Workflow
9
-
10
-
11
- # ── User operations ──────────────────────────────────────────────────
12
-
13
- async def create_user(
14
- session: AsyncSession,
15
- username: str,
16
- email: str,
17
- hashed_password: str,
18
- ) -> User:
19
- """Create and persist a new user."""
20
- user = User(
21
- username=username,
22
- email=email,
23
- hashed_password=hashed_password,
24
- )
25
- session.add(user)
26
- await session.commit()
27
- await session.refresh(user)
28
- return user
29
-
30
-
31
- async def get_user_by_username(session: AsyncSession, username: str) -> Optional[User]:
32
- """Look up a user by username."""
33
- result = await session.execute(select(User).where(User.username == username))
34
- return result.scalar_one_or_none()
35
-
36
-
37
- async def get_user_by_email(session: AsyncSession, email: str) -> Optional[User]:
38
- """Look up a user by email."""
39
- result = await session.execute(select(User).where(User.email == email))
40
- return result.scalar_one_or_none()
41
-
42
-
43
- async def get_user_by_id(session: AsyncSession, user_id: str) -> Optional[User]:
44
- """Look up a user by ID."""
45
- result = await session.execute(select(User).where(User.id == user_id))
46
- return result.scalar_one_or_none()
47
-
48
-
49
- # ── Workflow operations ──────────────────────────────────────────────
50
-
51
- async def create_workflow(
52
- session: AsyncSession,
53
- user_id: str,
54
- prompt: str,
55
- ) -> Workflow:
56
- """Create a new workflow record."""
57
- workflow = Workflow(user_id=user_id, prompt=prompt)
58
- session.add(workflow)
59
- await session.commit()
60
- await session.refresh(workflow)
61
- return workflow
62
-
63
-
64
- async def get_workflows_by_user(session: AsyncSession, user_id: str) -> list[Workflow]:
65
- """Return all workflows for a given user, newest first."""
66
- result = await session.execute(
67
- select(Workflow)
68
- .where(Workflow.user_id == user_id)
69
- .order_by(Workflow.created_at.desc())
70
- )
71
- return list(result.scalars().all())
72
-
73
-
74
- async def get_workflow_by_id(session: AsyncSession, workflow_id: str) -> Optional[Workflow]:
75
- """Return a single workflow by ID."""
76
- result = await session.execute(select(Workflow).where(Workflow.id == workflow_id))
77
- return result.scalar_one_or_none()
78
-
79
-
80
- # ── Task operations ──────────────────────────────────────────────────
81
-
82
- async def create_task(
83
- session: AsyncSession,
84
- workflow_id: str,
85
- task_type: str,
86
- input_data: Optional[str] = None,
87
- ) -> Task:
88
- """Create a new task within a workflow."""
89
- task = Task(workflow_id=workflow_id, task_type=task_type, input_data=input_data)
90
- session.add(task)
91
- await session.commit()
92
- await session.refresh(task)
93
- return task
94
-
95
-
96
- # ── Attempt operations ───────────────────────────────────────────────
97
-
98
- async def create_attempt(
99
- session: AsyncSession,
100
- task_id: str,
101
- attempt_number: int = 1,
102
- ) -> Attempt:
103
- """Create an execution attempt for a task."""
104
- attempt = Attempt(task_id=task_id, attempt_number=attempt_number)
105
- session.add(attempt)
106
- await session.commit()
107
- await session.refresh(attempt)
108
- return attempt
109
-
110
-
111
- # ── Event operations ─────────────────────────────────────────────────
112
-
113
- async def create_event(
114
- session: AsyncSession,
115
- workflow_id: str,
116
- event_type: str,
117
- payload: Optional[str] = None,
118
- ) -> Event:
119
- """Create a workflow event."""
120
- event = Event(workflow_id=workflow_id, event_type=event_type, payload=payload)
121
- session.add(event)
122
- await session.commit()
123
- await session.refresh(event)
124
- return event
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/events/__init__.py DELETED
@@ -1 +0,0 @@
1
- """AutoForge events package."""
 
 
backend/execution/__init__.py DELETED
@@ -1 +0,0 @@
1
- """AutoForge execution package."""
 
 
backend/llm/__init__.py DELETED
@@ -1 +0,0 @@
1
- # AutoForge LLM Providers
 
 
backend/llm/gemini.py DELETED
@@ -1,121 +0,0 @@
1
- """Google Gemini LLM provider."""
2
-
3
- import json
4
- from typing import Any, Optional
5
-
6
- import httpx
7
-
8
- from backend.config.settings import get_settings
9
- from backend.llm.provider import LLMProvider
10
-
11
-
12
- class GeminiProviderError(Exception):
13
- """Raised when the Gemini API returns an error."""
14
- pass
15
-
16
-
17
- class GeminiNotConfiguredError(GeminiProviderError):
18
- """Raised when the Gemini API key is not set."""
19
- pass
20
-
21
-
22
- class GeminiProvider(LLMProvider):
23
- """LLM provider backed by Google Gemini.
24
-
25
- Reads ``GEMINI_API_KEY`` and ``GEMINI_MODEL`` from application
26
- settings. Communicates with the Gemini REST API via ``httpx``.
27
- """
28
-
29
- BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
30
-
31
- def __init__(self) -> None:
32
- settings = get_settings()
33
- self.api_key = settings.gemini_api_key
34
- self.model = settings.gemini_model
35
-
36
- if not self.api_key:
37
- raise GeminiNotConfiguredError(
38
- "GEMINI_API_KEY is not set. "
39
- "Please configure it in your .env file."
40
- )
41
-
42
- # ── Public API ───────────────────────────────────────────────────
43
-
44
- async def generate(
45
- self,
46
- prompt: str,
47
- *,
48
- system_prompt: Optional[str] = None,
49
- temperature: float = 0.7,
50
- max_tokens: int = 4096,
51
- ) -> str:
52
- """Send a prompt to Gemini and return the generated text."""
53
- contents = []
54
- if system_prompt:
55
- contents.append({"role": "user", "parts": [{"text": system_prompt}]})
56
- contents.append({"role": "model", "parts": [{"text": "Understood."}]})
57
- contents.append({"role": "user", "parts": [{"text": prompt}]})
58
-
59
- body = {
60
- "contents": contents,
61
- "generationConfig": {
62
- "temperature": temperature,
63
- "maxOutputTokens": max_tokens,
64
- },
65
- }
66
-
67
- data = await self._request(body)
68
- return self._extract_text(data)
69
-
70
- async def generate_structured(
71
- self,
72
- prompt: str,
73
- schema: dict[str, Any],
74
- *,
75
- system_prompt: Optional[str] = None,
76
- ) -> dict[str, Any]:
77
- """Send a prompt to Gemini and parse the response as JSON."""
78
- schema_instruction = (
79
- f"Respond ONLY with valid JSON matching this schema:\n"
80
- f"```json\n{json.dumps(schema, indent=2)}\n```"
81
- )
82
- full_prompt = f"{prompt}\n\n{schema_instruction}"
83
- raw = await self.generate(full_prompt, system_prompt=system_prompt, temperature=0.2)
84
-
85
- # Strip markdown code fences if present
86
- text = raw.strip()
87
- if text.startswith("```"):
88
- text = text.split("\n", 1)[1] if "\n" in text else text[3:]
89
- if text.endswith("```"):
90
- text = text[:-3]
91
- text = text.strip()
92
-
93
- try:
94
- return json.loads(text)
95
- except json.JSONDecodeError as exc:
96
- raise GeminiProviderError(f"Failed to parse Gemini response as JSON: {exc}")
97
-
98
- # ── Internals ────────────────────────────────────────────────────
99
-
100
- async def _request(self, body: dict) -> dict:
101
- """Make a POST request to the Gemini generateContent endpoint."""
102
- url = (
103
- f"{self.BASE_URL}/models/{self.model}:generateContent"
104
- f"?key={self.api_key}"
105
- )
106
-
107
- async with httpx.AsyncClient(timeout=60.0) as client:
108
- response = await client.post(url, json=body)
109
- if response.status_code != 200:
110
- raise GeminiProviderError(
111
- f"Gemini API error {response.status_code}: {response.text}"
112
- )
113
- return response.json()
114
-
115
- @staticmethod
116
- def _extract_text(data: dict) -> str:
117
- """Extract generated text from the Gemini response payload."""
118
- try:
119
- return data["candidates"][0]["content"]["parts"][0]["text"]
120
- except (KeyError, IndexError) as exc:
121
- raise GeminiProviderError(f"Unexpected Gemini response structure: {exc}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/llm/provider.py DELETED
@@ -1,55 +0,0 @@
1
- """Abstract LLM provider interface."""
2
-
3
- from abc import ABC, abstractmethod
4
- from typing import Any, Optional
5
-
6
-
7
- class LLMProvider(ABC):
8
- """Base class for all LLM providers.
9
-
10
- Every concrete provider must implement ``generate`` and
11
- ``generate_structured`` so the rest of AutoForge can remain
12
- provider-agnostic.
13
- """
14
-
15
- @abstractmethod
16
- async def generate(
17
- self,
18
- prompt: str,
19
- *,
20
- system_prompt: Optional[str] = None,
21
- temperature: float = 0.7,
22
- max_tokens: int = 4096,
23
- ) -> str:
24
- """Generate a free-form text completion.
25
-
26
- Args:
27
- prompt: The user-facing prompt.
28
- system_prompt: Optional system-level instruction.
29
- temperature: Sampling temperature.
30
- max_tokens: Maximum tokens in the response.
31
-
32
- Returns:
33
- The generated text.
34
- """
35
- ...
36
-
37
- @abstractmethod
38
- async def generate_structured(
39
- self,
40
- prompt: str,
41
- schema: dict[str, Any],
42
- *,
43
- system_prompt: Optional[str] = None,
44
- ) -> dict[str, Any]:
45
- """Generate output conforming to a JSON schema.
46
-
47
- Args:
48
- prompt: The user-facing prompt.
49
- schema: A JSON Schema the response must match.
50
- system_prompt: Optional system-level instruction.
51
-
52
- Returns:
53
- A dictionary matching the provided schema.
54
- """
55
- ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/main.py DELETED
@@ -1,64 +0,0 @@
1
- """AutoForge FastAPI application entry point."""
2
-
3
- import os
4
- from contextlib import asynccontextmanager
5
- from pathlib import Path
6
-
7
- from fastapi import FastAPI
8
- from fastapi.middleware.cors import CORSMiddleware
9
- from fastapi.staticfiles import StaticFiles
10
-
11
- from backend.api.auth import router as auth_router
12
- from backend.api.health import router as health_router
13
- from backend.api.workflows import router as workflows_router
14
- from backend.config.settings import get_settings
15
- from backend.db.database import init_db
16
-
17
-
18
- @asynccontextmanager
19
- async def lifespan(app: FastAPI):
20
- """Startup and shutdown lifecycle."""
21
- # Ensure data directory exists
22
- os.makedirs("data", exist_ok=True)
23
- # Initialize database tables
24
- await init_db()
25
- yield
26
-
27
-
28
- app = FastAPI(
29
- title="AutoForge",
30
- description="Autonomous AI coding system",
31
- version="0.1.0",
32
- lifespan=lifespan,
33
- )
34
-
35
- # CORS — permissive for local development
36
- app.add_middleware(
37
- CORSMiddleware,
38
- allow_origins=["*"],
39
- allow_credentials=True,
40
- allow_methods=["*"],
41
- allow_headers=["*"],
42
- )
43
-
44
- # API routers
45
- app.include_router(health_router)
46
- app.include_router(auth_router)
47
- app.include_router(workflows_router)
48
-
49
- # Serve frontend static files
50
- frontend_path = Path(__file__).resolve().parent.parent / "frontend"
51
- if frontend_path.is_dir():
52
- app.mount("/", StaticFiles(directory=str(frontend_path), html=True), name="frontend")
53
-
54
-
55
- if __name__ == "__main__":
56
- import uvicorn
57
-
58
- settings = get_settings()
59
- uvicorn.run(
60
- "backend.main:app",
61
- host=settings.host,
62
- port=settings.port,
63
- reload=settings.debug,
64
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/orchestration/__init__.py DELETED
@@ -1 +0,0 @@
1
- """AutoForge orchestration package."""