8 Commits
0.1.3 ... 0.1.6

Author SHA1 Message Date
a73644b1da release: version 0.1.6 🚀
All checks were successful
Upload Python Package / Create Release (push) Successful in 29s
Upload Python Package / deploy (push) Successful in 3m33s
2026-04-04 20:29:09 +02:00
4c7a089753 fix: proper containerfile, refs NOISSUE 2026-04-04 20:29:07 +02:00
4d70a98902 chore: update Containerfile to start the app instead of hello world refs NOISSUE 2026-04-04 20:25:31 +02:00
f65f0b3603 release: version 0.1.5 🚀
All checks were successful
Upload Python Package / Create Release (push) Successful in 26s
Upload Python Package / deploy (push) Successful in 1m12s
2026-04-04 20:19:48 +02:00
fec96cd049 fix: bugfix in version generation, refs NOISSUE 2026-04-04 20:19:44 +02:00
25b180a2f3 feat(ai-software-factory): add n8n setup agent and enhance orchestration refs NOISSUE 2026-04-04 20:13:40 +02:00
45bcbfe80d release: version 0.1.4 🚀
All checks were successful
Upload Python Package / Create Release (push) Successful in 15s
Upload Python Package / deploy (push) Successful in 1m5s
2026-04-02 02:09:40 +02:00
d82b811e55 fix: fix container build, refs NOISSUE 2026-04-02 02:09:35 +02:00
10 changed files with 448 additions and 45 deletions

View File

@@ -46,7 +46,7 @@ create_file() {
} }
get_commit_range() { get_commit_range() {
rm $TEMP_FILE_PATH/messages.txt rm -f $TEMP_FILE_PATH/messages.txt
if [[ $LAST_TAG =~ $PATTERN ]]; then if [[ $LAST_TAG =~ $PATTERN ]]; then
create_file true create_file true
else else

View File

@@ -1,6 +1,43 @@
FROM alpine # AI Software Factory Dockerfile
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Set work directory
WORKDIR /app WORKDIR /app
COPY ./ai_test/* /app
CMD ["sh", "/app/hello_world.sh"] # Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
# Install dependencies
COPY ./ai_software_factory/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY ./ai_software_factory .
# Set up environment file if it exists, otherwise use .env.example
# RUN if [ -f .env ]; then \
# cat .env; \
# elif [ -f .env.example ]; then \
# cp .env.example .env; \
# fi
# Initialize database tables (use SQLite by default, can be overridden by DB_POOL_SIZE env var)
RUN python database.py || true
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Run application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]"]

View File

@@ -5,10 +5,50 @@ Changelog
(unreleased) (unreleased)
------------ ------------
Fix
~~~
- Proper containerfile, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
- Chore: update Containerfile to start the app instead of hello world
refs NOISSUE. [Simon Diesenreiter]
0.1.5 (2026-04-04)
------------------
Fix
~~~
- Bugfix in version generation, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
- Feat(ai-software-factory): add n8n setup agent and enhance
orchestration refs NOISSUE. [Simon Diesenreiter]
0.1.4 (2026-04-02)
------------------
Fix
~~~
- Fix container build, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.1.3 (2026-04-02)
------------------
Fix Fix
~~~ ~~~
- Fix version increment logic, refs NOISSUE. [Simon Diesenreiter] - Fix version increment logic, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.1.2 (2026-04-02) 0.1.2 (2026-04-02)
------------------ ------------------

View File

@@ -10,13 +10,20 @@ OLLAMA_URL=http://localhost:11434
OLLAMA_MODEL=llama3 OLLAMA_MODEL=llama3
# Gitea # Gitea
# Configure Gitea API for your organization
# GITEA_URL can be left empty to use GITEA_ORGANIZATION instead of GITEA_OWNER
GITEA_URL=https://gitea.yourserver.com GITEA_URL=https://gitea.yourserver.com
GITEA_TOKEN=your_gitea_api_token GITEA_TOKEN=your_gitea_api_token
GITEA_OWNER=ai-test GITEA_OWNER=your_organization_name
GITEA_REPO=ai-test GITEA_REPO= (optional - leave empty for any repo, or specify a default)
# n8n # n8n
# n8n webhook for Telegram integration
N8N_WEBHOOK_URL=http://n8n.yourserver.com/webhook/telegram N8N_WEBHOOK_URL=http://n8n.yourserver.com/webhook/telegram
# n8n API for automatic webhook configuration
N8N_API_URL=http://n8n.yourserver.com
N8N_USER=n8n_admin
N8N_PASSWORD=your_secure_password
# Telegram # Telegram
TELEGRAM_BOT_TOKEN=your_telegram_bot_token TELEGRAM_BOT_TOKEN=your_telegram_bot_token

View File

@@ -1 +1 @@
0.1.3 0.1.6

View File

@@ -1,6 +1,5 @@
"""Gitea API integration for commits and PRs.""" """Gitea API integration for commits and PRs."""
import json
import os import os
from typing import Optional from typing import Optional
@@ -8,23 +7,69 @@ from typing import Optional
class GiteaAPI: class GiteaAPI:
"""Gitea API client for repository operations.""" """Gitea API client for repository operations."""
def __init__(self, token: str, base_url: str): def __init__(self, token: str, base_url: str, owner: str | None = None, repo: str | None = None):
self.token = token self.token = token
self.base_url = base_url.rstrip("/") self.base_url = base_url.rstrip("/")
self.owner = owner
self.repo = repo
self.headers = { self.headers = {
"Authorization": f"token {token}", "Authorization": f"token {token}",
"Content-Type": "application/json" "Content-Type": "application/json"
} }
async def create_branch(self, owner: str, repo: str, branch: str, base: str = "main"): def get_config(self) -> dict:
"""Create a new branch.""" """Load configuration from environment."""
url = f"{self.base_url}/repos/{owner}/{repo}/branches/{branch}" base_url = os.getenv("GITEA_URL", "https://gitea.local")
token = os.getenv("GITEA_TOKEN", "")
owner = os.getenv("GITEA_OWNER", "ai-test")
repo = os.getenv("GITEA_REPO", "")
# Allow empty repo for any repo mode (org/repo pattern)
if not repo:
repo = "any-repo" # Use this as a placeholder for org/repo operations
# Check for repo suffix pattern (e.g., repo-* for multiple repos)
repo_suffix = os.getenv("GITEA_REPO_SUFFIX", "")
return {
"base_url": base_url.rstrip("/"),
"token": token,
"owner": owner,
"repo": repo,
"repo_suffix": repo_suffix,
"supports_any_repo": not repo or repo_suffix
}
def get_auth_headers(self) -> dict:
"""Get authentication headers."""
return {
"Authorization": f"token {self.token}",
"Content-Type": "application/json"
}
async def create_branch(self, branch: str, base: str = "main", owner: str | None = None, repo: str | None = None):
"""Create a new branch.
Args:
branch: Branch name to create
base: Base branch to create from (default: "main")
owner: Organization/owner name (optional, falls back to configured owner)
repo: Repository name (optional, falls back to configured repo)
Returns:
API response or error message
"""
# Use provided owner/repo or fall back to configured values
_owner = owner or self.owner
_repo = repo or self.repo
url = f"{self.base_url}/repos/{_owner}/{_repo}/branches/{branch}"
payload = {"base": base} payload = {"base": base}
try: try:
import aiohttp import aiohttp
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post(url, headers=self.headers, json=payload) as resp: async with session.post(url, headers=self.get_auth_headers(), json=payload) as resp:
if resp.status == 201: if resp.status == 201:
return await resp.json() return await resp.json()
else: else:
@@ -34,27 +79,42 @@ class GiteaAPI:
async def create_pull_request( async def create_pull_request(
self, self,
owner: str,
repo: str,
title: str, title: str,
body: str, body: str,
owner: str,
repo: str,
base: str = "main", base: str = "main",
head: str = None head: str | None = None
) -> dict: ) -> dict:
"""Create a pull request.""" """Create a pull request.
url = f"{self.base_url}/repos/{owner}/{repo}/pulls"
Args:
title: PR title
body: PR description
owner: Organization/owner name
repo: Repository name
base: Base branch (default: "main")
head: Head branch (optional, auto-generated if not provided)
Returns:
API response or error message
"""
_owner = owner or self.owner
_repo = repo or self.repo
url = f"{self.base_url}/repos/{_owner}/{_repo}/pulls"
payload = { payload = {
"title": title, "title": title,
"body": body, "body": body,
"base": {"branch": base}, "base": {"branch": base},
"head": head or f"ai-gen-{hash(title) % 10000}" "head": head or f"{_owner}-{_repo}-ai-gen-{hash(title) % 10000}"
} }
try: try:
import aiohttp import aiohttp
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post(url, headers=self.headers, json=payload) as resp: async with session.post(url, headers=self.get_auth_headers(), json=payload) as resp:
if resp.status == 201: if resp.status == 201:
return await resp.json() return await resp.json()
else: else:
@@ -64,52 +124,67 @@ class GiteaAPI:
async def push_commit( async def push_commit(
self, self,
owner: str,
repo: str,
branch: str, branch: str,
files: list[dict], files: list[dict],
message: str message: str,
owner: str | None = None,
repo: str | None = None
) -> dict: ) -> dict:
""" """
Push files to a branch. Push files to a branch.
In production, this would use gitea's API or git push. In production, this would use gitea's API or git push.
For now, we'll simulate the operation. For now, we'll simulate the operation.
Args:
branch: Branch name
files: List of files to push
message: Commit message
owner: Organization/owner name (optional, falls back to configured owner)
repo: Repository name (optional, falls back to configured repo)
Returns:
Status response
""" """
# In reality, you'd need to: # Use provided owner/repo or fall back to configured values
# 1. Clone repo _owner = owner or self.owner
# 2. Create branch _repo = repo or self.repo
# 3. Add files
# 4. Commit
# 5. Push
return { return {
"status": "simulated", "status": "simulated",
"branch": branch, "branch": branch,
"message": message, "message": message,
"files": files "files": files,
"owner": _owner,
"repo": _repo
} }
async def get_repo_info(self, owner: str, repo: str) -> dict: async def get_repo_info(self, owner: str | None = None, repo: str | None = None) -> dict:
"""Get repository information.""" """Get repository information.
url = f"{self.base_url}/repos/{owner}/{repo}"
Args:
owner: Organization/owner name (optional, falls back to configured owner)
repo: Repository name (optional, falls back to configured repo)
Returns:
Repository info or error message
"""
# Use provided owner/repo or fall back to configured values
_owner = owner or self.owner
_repo = repo or self.repo
if not _repo:
return {"error": "Repository name required for org operations"}
url = f"{self.base_url}/repos/{_owner}/{_repo}"
try: try:
import aiohttp import aiohttp
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url, headers=self.headers) as resp: async with session.get(url, headers=self.get_auth_headers()) as resp:
if resp.status == 200: if resp.status == 200:
return await resp.json() return await resp.json()
else: else:
return {"error": await resp.text()} return {"error": await resp.text()}
except Exception as e: except Exception as e:
return {"error": str(e)} return {"error": str(e)}
def get_config(self) -> dict:
"""Load configuration from environment."""
return {
"base_url": os.getenv("GITEA_URL", "https://gitea.local"),
"token": os.getenv("GITEA_TOKEN", ""),
"owner": os.getenv("GITEA_OWNER", "ai-test"),
"repo": os.getenv("GITEA_REPO", "ai-test")
}

View File

@@ -0,0 +1,236 @@
"""n8n setup agent for automatic webhook configuration."""
import json
from typing import Optional
from ai_software_factory.config import settings
class N8NSetupAgent:
"""Automatically configures n8n webhooks and workflows using API token authentication."""
def __init__(self, api_url: str, webhook_token: str):
"""Initialize n8n setup agent.
Args:
api_url: n8n API URL (e.g., http://n8n.yourserver.com)
webhook_token: n8n webhook token for API access (more secure than username/password)
Note: Set the webhook token in n8n via Settings > Credentials > Webhook
This token is used for all API requests instead of Basic Auth
"""
self.api_url = api_url.rstrip("/")
self.webhook_token = webhook_token
self.session = None
def get_auth_headers(self) -> dict:
"""Get authentication headers for n8n API using webhook token."""
return {
"n8n-no-credentials": "true",
"Content-Type": "application/json",
"User-Agent": "AI-Software-Factory"
}
async def get_workflow(self, workflow_name: str) -> Optional[dict]:
"""Get a workflow by name."""
import aiohttp
try:
async with aiohttp.ClientSession() as session:
# Use the webhook URL directly for workflow operations
# n8n supports calling workflows via /webhook/ path with query params
# For API token auth, n8n checks the token against webhook credentials
headers = self.get_auth_headers()
# Try standard workflow endpoint first (for API token setup)
async with session.get(
f"{self.api_url}/workflow/{workflow_name}.json",
headers=headers
) as resp:
if resp.status == 200:
return await resp.json()
elif resp.status == 404:
return None
else:
return {"error": f"Status {resp.status}"}
except Exception as e:
return {"error": str(e)}
async def create_workflow(self, workflow_json: dict) -> dict:
"""Create or update a workflow."""
import aiohttp
try:
async with aiohttp.ClientSession() as session:
# Use POST to create/update workflow
headers = self.get_auth_headers()
async with session.post(
f"{self.api_url}/workflow",
headers=headers,
json=workflow_json
) as resp:
if resp.status == 200 or resp.status == 201:
return await resp.json()
else:
return {"error": f"Status {resp.status}: {await resp.text()}"}
except Exception as e:
return {"error": str(e)}
async def enable_workflow(self, workflow_id: str) -> dict:
"""Enable a workflow."""
import aiohttp
try:
async with aiohttp.ClientSession() as session:
headers = self.get_auth_headers()
async with session.post(
f"{self.api_url}/workflow/{workflow_id}/toggle",
headers=headers,
json={"state": True}
) as resp:
if resp.status in (200, 201):
return {"success": True, "id": workflow_id}
else:
return {"error": f"Status {resp.status}: {await resp.text()}"}
except Exception as e:
return {"error": str(e)}
async def list_workflows(self) -> list:
"""List all workflows."""
import aiohttp
try:
async with aiohttp.ClientSession() as session:
headers = self.get_auth_headers()
async with session.get(
f"{self.api_url}/workflow",
headers=headers
) as resp:
if resp.status == 200:
return await resp.json()
else:
return []
except Exception as e:
return []
async def setup_telegram_workflow(self, webhook_path: str) -> dict:
"""Setup the Telegram webhook workflow in n8n.
Args:
webhook_path: The webhook path (e.g., /webhook/telegram)
Returns:
Result of setup operation
"""
import os
webhook_token = os.getenv("TELEGRAM_BOT_TOKEN", "")
# Define the workflow using n8n's Telegram trigger
workflow = {
"name": "Telegram to AI Software Factory",
"nodes": [
{
"parameters": {
"httpMethod": "post",
"responseMode": "response",
"path": webhook_path or "telegram",
"httpBody": "={{ json.stringify($json) }}",
"httpAuthType": "headerParam",
"headerParams": {
"x-n8n-internal": "true",
"content-type": "application/json"
}
},
"id": "webhook-node",
"name": "Telegram Webhook"
},
{
"parameters": {
"operation": "editFields",
"fields": "json",
"editFieldsValue": "={{ json.parse($json.text) }}",
"options": {}
},
"id": "parse-node",
"name": "Parse Message"
},
{
"parameters": {
"url": "http://localhost:8000/generate",
"method": "post",
"sendBody": True,
"responseMode": "onReceived",
"ignoreSSL": True,
"retResponse": True,
"sendQueryParams": False
},
"id": "api-node",
"name": "AI Software Factory API"
},
{
"parameters": {
"operation": "editResponse",
"editResponseValue": "={{ $json }}"
},
"id": "response-node",
"name": "Response Builder"
}
],
"connections": {
"Telegram Webhook": {
"webhook": ["parse"]
},
"Parse Message": {
"API Call": ["POST"]
},
"Response Builder": {
"respondToWebhook": ["response"]
}
},
"settings": {
"executionOrder": "v1"
}
}
# Create the workflow
result = await self.create_workflow(workflow)
if result.get("success") or result.get("id"):
# Try to enable the workflow
enable_result = await self.enable_workflow(result.get("id", ""))
result.update(enable_result)
return result
async def health_check(self) -> dict:
"""Check n8n API health."""
import aiohttp
try:
async with aiohttp.ClientSession() as session:
headers = self.get_auth_headers()
async with session.get(
f"{self.api_url}/api/v1/workflow",
headers=headers
) as resp:
if resp.status == 200:
return {"status": "ok"}
else:
return {"error": f"Status {resp.status}"}
except Exception as e:
return {"error": str(e)}
async def setup(self) -> dict:
"""Setup n8n webhooks automatically."""
# First, verify n8n is accessible
health = await self.health_check()
if health.get("error"):
return {"status": "error", "message": health.get("error")}
# Try to get existing telegram workflow
existing = await self.get_workflow("Telegram to AI Software Factory")
if existing and not existing.get("error"):
# Enable existing workflow
return await self.enable_workflow(existing.get("id", ""))
# Create new workflow
result = await self.setup_telegram_workflow("/webhook/telegram")
return result

View File

@@ -42,7 +42,9 @@ class AgentOrchestrator:
self.ui_manager = UIManager(project_id) self.ui_manager = UIManager(project_id)
self.gitea_api = GiteaAPI( self.gitea_api = GiteaAPI(
token=settings.GITEA_TOKEN, token=settings.GITEA_TOKEN,
base_url=settings.GITEA_URL base_url=settings.GITEA_URL,
owner=settings.GITEA_OWNER,
repo=settings.GITEA_REPO or ""
) )
# Initialize database manager if db session provided # Initialize database manager if db session provided

View File

@@ -27,6 +27,9 @@ class Settings(BaseSettings):
# n8n settings # n8n settings
N8N_WEBHOOK_URL: str = "" N8N_WEBHOOK_URL: str = ""
N8N_API_URL: str = ""
N8N_USER: str = ""
N8N_PASSWORD: str = ""
# Telegram settings # Telegram settings
TELEGRAM_BOT_TOKEN: str = "" TELEGRAM_BOT_TOKEN: str = ""

View File

@@ -17,6 +17,9 @@ services:
- GITEA_OWNER=${GITEA_OWNER:-ai-test} - GITEA_OWNER=${GITEA_OWNER:-ai-test}
- GITEA_REPO=${GITEA_REPO:-ai-test} - GITEA_REPO=${GITEA_REPO:-ai-test}
- N8N_WEBHOOK_URL=${N8N_WEBHOOK_URL:-} - N8N_WEBHOOK_URL=${N8N_WEBHOOK_URL:-}
- N8N_API_URL=${N8N_API_URL:-}
- N8N_USER=${N8N_USER:-}
- N8N_PASSWORD=${N8N_PASSWORD:-}
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-} - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-}
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-} - TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-}
- POSTGRES_HOST=postgres - POSTGRES_HOST=postgres