Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 208 additions & 0 deletions .github/workflows/issue-triage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
name: Auto-label and assign issues

on:
issues:
types: [opened]

permissions:
issues: write

jobs:
triage:
runs-on: ubuntu-latest
steps:
- name: Triage issue
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const issue = context.payload.issue;
const title = (issue.title || '').toLowerCase();
const body = (issue.body || '').toLowerCase();
const text = `${title} ${body}`;

const labels = new Set();
let assignees = new Set();

// ── 1. Issue-type labels ──────────────────────────────────
const bugPatterns = [
'bug', 'error', 'crash', 'fail', 'broken', 'not working',
'exception', 'traceback', 'stacktrace', 'typeerror',
'keyerror', 'valueerror', 'attributeerror', '500',
'unexpected', 'regression', 'does not work', "doesn't work",
'cannot', "can't", 'issue with'
];
const featurePatterns = [
'feature', 'enhancement', 'request', 'add support',
'would be nice', 'proposal', 'improve', 'new option',
'should support', 'wish', 'idea', 'suggestion'
];
const docPatterns = [
'documentation', 'docs', 'readme', 'typo in doc',
'missing docs', 'unclear', 'example', 'tutorial',
'how to', 'howto', 'instructions'
];
const questionPatterns = [
'question', 'how do i', 'how can i', 'is it possible',
'what is the', 'help me', 'guidance', 'confused',
'explain', 'clarification'
];

if (bugPatterns.some(p => text.includes(p))) {
labels.add('bug');
}
if (featurePatterns.some(p => text.includes(p))) {
labels.add('enhancement');
}
if (docPatterns.some(p => text.includes(p))) {
labels.add('documentation');
}
if (questionPatterns.some(p => text.includes(p))) {
labels.add('question');
}
// If nothing matched above, default to bug for issues that
// mention errors, otherwise enhancement
if (labels.size === 0) {
labels.add('enhancement');
}

// ── 2. Component labels ───────────────────────────────────
const frontendPatterns = [
'frontend', 'next.js', 'nextjs', 'react', 'typescript',
'tailwind', 'css', 'ui', 'ux', 'component', 'page',
'pwa', 'serwist', 'vercel', 'browser', 'layout',
'responsive', 'mobile', 'dark mode', 'calendar view',
'upload page', 'login page', 'signup page'
];
const backendPatterns = [
'backend', 'fastapi', 'api', 'endpoint', 'router',
'database', 'sqlite', 'sqlmodel', 'auth', 'jwt',
'cookie', 'session', 'cors', 'sse', 'server',
'uvicorn', 'railway', 'render', '/letters', '/actions',
'/chat', '/health', 'migration', 'schema'
];
const aiAgentPatterns = [
'ocr', 'qwen-vl', 'react agent', 'langgraph', 'agent',
'classification', 'risk score', 'tavily', 'search tool',
'image processing', 'letter type', 'extraction',
'qwen', 'llm', 'prompt', 'form fill', 'form_fill'
];
const aiRagPatterns = [
'rag', 'chromadb', 'chroma', 'embedding', 'retrieval',
'legal text', 'corpus', 'ingestion', 'ingest',
'citation', 'law', 'aufenthg', 'asylg', 'sgb',
'bafög', 'bafoeg', 'response draft', 'response generation',
'hallucination', 'grounding', 'checklist'
];
const infraPatterns = [
'ci', 'cd', 'ci/cd', 'deploy', 'docker', 'github action',
'workflow', 'pipeline', 'env var', 'environment',
'config', 'build', 'release', 'infra', 'devops'
];

if (frontendPatterns.some(p => text.includes(p))) {
labels.add('component: frontend');
assignees.add('saintnuno'); // Dev 1: Frontend
}
if (backendPatterns.some(p => text.includes(p))) {
labels.add('component: backend');
assignees.add('aircode610'); // Dev 2: Backend
}
if (aiAgentPatterns.some(p => text.includes(p))) {
labels.add('component: ai-agent');
assignees.add('danialeyz'); // Dev 4: OCR + ReAct
}
if (aiRagPatterns.some(p => text.includes(p))) {
labels.add('component: ai-rag');
assignees.add('Alir3zag'); // Dev 3: RAG pipeline
}
if (infraPatterns.some(p => text.includes(p))) {
labels.add('component: infra');
assignees.add('aircode610'); // Repo owner
}

// ── 3. Priority labels ────────────────────────────────────
const criticalPatterns = [
'critical', 'urgent', 'blocker', 'system down',
'data loss', 'security vulnerability', 'production down',
'p0', 'outage'
];
const highPatterns = [
'important', 'high priority', 'blocks', 'blocking',
'severe', 'major', 'p1', 'asap', 'deadline missed',
'cannot deploy'
];
const lowPatterns = [
'low priority', 'minor', 'cosmetic', 'nice to have',
'nit', 'trivial', 'p3', 'when possible', 'polish'
];

if (criticalPatterns.some(p => text.includes(p))) {
labels.add('priority: critical');
} else if (highPatterns.some(p => text.includes(p))) {
labels.add('priority: high');
} else if (lowPatterns.some(p => text.includes(p))) {
labels.add('priority: low');
} else {
labels.add('priority: medium');
}

// ── 4. Security label ─────────────────────────────────────
const securityPatterns = [
'security', 'vulnerability', 'cve', 'xss', 'csrf',
'injection', 'exploit', 'leak', 'exposed secret',
'auth bypass', 'privilege escalation'
];
if (securityPatterns.some(p => text.includes(p))) {
labels.add('security');
assignees.add('aircode610'); // Owner handles security
}

// ── 5. Fallback assignment ────────────────────────────────
// If no component was matched, assign to repo owner for triage
if (assignees.size === 0) {
assignees.add('aircode610');
}

// ── 6. Apply labels ───────────────────────────────────────
const labelArray = [...labels];
if (labelArray.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: labelArray,
});
core.info(`Applied labels: ${labelArray.join(', ')}`);
}

// ── 7. Assign users ───────────────────────────────────────
const assigneeArray = [...assignees];
if (assigneeArray.length > 0) {
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
assignees: assigneeArray,
});
core.info(`Assigned to: ${assigneeArray.join(', ')}`);
}

// ── 8. Post triage comment ────────────────────────────────
const commentBody = [
`👋 Thanks for opening this issue!`,
``,
`**Auto-triage summary:**`,
`- **Labels:** ${labelArray.map(l => '`' + l + '`').join(', ')}`,
`- **Assigned to:** ${assigneeArray.map(a => '@' + a).join(', ')}`,
``,
`> This was automatically triaged based on the issue content. ` +
`If the labels or assignment look wrong, please update them manually.`,
].join('\n');

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: commentBody,
});
45 changes: 45 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Lint Python

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install ruff
run: pip install ruff

- name: Ruff check
run: ruff check backend/ ai/

- name: Ruff format check
run: ruff format --check backend/ ai/

tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: pip install -r backend/requirements.txt pytest pytest-asyncio

- name: Run tests
working-directory: backend
env:
DATABASE_URL: "sqlite:///test.db"
JWT_SECRET: "test-secret-test-secret-test-secret-32"
run: python -m pytest tests/ -v
8 changes: 4 additions & 4 deletions ai/form_fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
import httpx

DASHSCOPE_API_KEY = os.environ.get("DASHSCOPE_API_KEY", "")
DASHSCOPE_INTL_URL = (
"https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
)
DASHSCOPE_INTL_URL = "https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"

# Standard placeholder patterns for common German form fields
PLACEHOLDER_MAP = {
Expand Down Expand Up @@ -70,7 +68,9 @@ async def generate_filled_form(
b64 = base64.b64encode(image_bytes).decode()

ext = image_path.rsplit(".", 1)[-1].lower()
mime = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png"}.get(ext, "image/jpeg")
mime = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png"}.get(
ext, "image/jpeg"
)
data_url = f"data:{mime};base64,{b64}"

field_instructions = _build_field_instructions(placeholders)
Expand Down
14 changes: 11 additions & 3 deletions ai/rag/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@
QWEN_AGENT_MODEL = os.environ.get("QWEN_AGENT_MODEL", "qwen3.7-plus")

LANGUAGE_NAMES = {
"en": "English", "de": "German", "tr": "Turkish", "ar": "Arabic",
"es": "Spanish", "fr": "French", "zh": "Chinese", "fa": "Persian",
"en": "English",
"de": "German",
"tr": "Turkish",
"ar": "Arabic",
"es": "Spanish",
"fr": "French",
"zh": "Chinese",
"fa": "Persian",
}

_model = ChatOpenAI(
Expand All @@ -34,7 +40,9 @@ async def generate_response(
language: str = "en",
) -> GenerationOutput:
"""Retrieve legal context from ChromaDB, inject into prompt, return structured output."""
legal_context = retrieve_as_context(agent_result.letter_type, agent_result.consequence)
legal_context = retrieve_as_context(
agent_result.letter_type, agent_result.consequence
)

prompt = GENERATION_PROMPT.format(
ocr_text=ocr_text[:3000],
Expand Down
Loading
Loading