Developer: # 목적 - 셀 환경에서 실행 가능한, 간단하고 유용한 LLM 스크립트(예: one-liner 또는 짧은 커맨드)를 작성하려고 합니다. - LLM에게 간단한 질문을 던지는 방식으로 활용하고자 합니다. # 요청 사항 - 우수한 예시로 어떤 LLM 스크립트들이 있는지 궁금합니다. - 셀 환경에서 손쉽게 실행 가능하며, 실제 실무에 도움이 되는 스크립트 사용 예시를 알려주세요. # 예시(예상) - 예: 번역, 요약, 코드 설명 등 - 예: 특정 파일 내 주요 내용 요약 스크립트, 자동 코드 문서화 등 시작 전, 다음과 같은 체크리스트(3-7개)를 간결하게 작성하세요. 구현 세부 내용이 아닌 개념 단계로 구성합니다. 스크립트나 코드 예시가 포함된다면, 다음을 명확히 하세요: (1) 전제 가정 명시, (2) 재현 가능한 예시 또는 테스트 케이스 포함, (3) 일관된 스타일 적용. 각 스크립트 예시에는 목적 및 입력/출력 설명을 간단히 덧붙이세요.

아래 내용은 “셀에서 바로 쓰는, 짧고 유용한 LLM 스크립트”에 초점을 맞춰 정리했습니다. (요청하신 대로, 단계별로 사고하고 구성했어요—다만 내부 추론 전개 대신 실행 가능한 절차로 분해해 드립니다.)

시작 전 체크리스트 (개념)

  1. 목적 고정 : 요약/번역/설명/생성 등 각 스크립트의 1문장 목표를 먼저 정하세요.

  2. 입력 경계 : 표준입력/파일/git diff 등 입력 경로를 명확히 하고, 길면 잘라서 보내세요(예: head -c 12000).

  3. 출력 약속 : 결과 형식을 미리 정하세요(한글/불릿/JSON 등) → 파이프라인에서 jq, sed, awk로 후처리.

  4. 일관 프롬프트 : 톤/언어/길이/포맷을 프롬프트 초반에 고정(“한국어, 핵심만, 5줄 불릿” 등).

  5. 재현성 : 예시는 가정(백엔드/모델/키)를 먼저 밝히고, 테스트 케이스 를 포함합니다.

  6. 보안/비용 : 키는 환경변수로, 대용량은 로컬 요약→핵심만 업로드, 민감 데이터는 로컬 모델(Ollama) 고려.

  7. 장애 대비 : 실패 시 재시도/타임아웃/최대 토큰/온도값(=랜덤성) 같은 기본 파라미터를 명시.


전제(공통)

  • OpenAI API 사용 시 : export OPENAI_API_KEY=...export OPENAI_MODEL=gpt-5 (계정에서 사용 가능한 최신 모델로 지정). 엔드포인트는 Responses API /v1/responses 입니다. OpenAI 플랫폼+3

  • 로컬 대안(Ollama) : ollama 설치 후 ollama run <모델> 또는 REST POST :11434/api/generate. 예: codellama, mistral:instruct, llama2:chat. Ollama+4

아래 모든 스크립트는 OpenAI 버전(curl)Ollama 버전 을 나란히 제공합니다.
jq로 결과를 뽑을 땐 신/구 응답 포맷 호환을 위해 output_textchoices[0].message.content 순서로 폴백합니다.


1) 단문 질의응답(Ad-hoc Q&A)

목적 : 빠른 질문→짧은 답.
입력/출력 : echo "질문" → 한글 5문장 이내 답.
가정 : 작은 프롬프트(수백~수천 자).
테스트 : “PostgreSQL 인덱스가 동작하는 핵심 원리를 5문장으로.”

OpenAI

echo '한국어로 간결히. PostgreSQL 인덱스 핵심 원리를 5문장으로 요약해줘.' | \
curl -s https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
  -d @- | jq -r '.output_text // .choices[0].message.content'

Ollama

echo '한국어로 간결히. PostgreSQL 인덱스 핵심 원리를 5문장으로 요약해줘.' | \
curl -s http://localhost:11434/api/generate \
  -d '{"model":"codellama:7b-instruct","prompt": "'"$(cat -)"'"}' | jq -r '.response'

(Responses API 개요/예시는 공식 문서 참고. OpenAI 플랫폼+1)


2) 번역(one-liner 번역 파이프)

목적 : 영어↔한국어 문단 번역.
입력/출력 : 파일/표준입력 → 원문 보존 없이 번역문만.
가정 : 짧은 텍스트(긴 경우 1만~2만 바이트로 잘라 전송).
테스트 : 아래 sample_en.txt 생성 후 실행.

cat > sample_en.txt <<'TXT'
PostgreSQL uses B-tree indexes by default. They keep keys sorted and enable fast lookups.
TXT
head -c 12000 sample_en.txt | sed '1s/^/다음 영어를 한국어로 자연스럽게 번역: \n/' | \
curl -s https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
  -d @- | jq -r '.output_text // .choices[0].message.content'

Ollama

head -c 12000 sample_en.txt | sed '1s/^/다음 영어를 한국어로 자연스럽게 번역: \n/' | \
curl -s http://localhost:11434/api/generate \
  -d '{"model":"mistral:instruct","prompt": "'"$(cat -)"'"}' | jq -r '.response'

3) 파일 요약(불릿 7줄)

목적 : 긴 텍스트 요약(회의록/리포트).
입력/출력 : 텍스트 파일 → 7불릿 핵심 포인트.
가정 : 문서가 길면 부분 요약→최종 요약(map-reduce).
테스트 : notes.txt 생성.

cat > notes.txt <<'TXT'
We discussed migration steps, risks (downtime), and rollout plan with canary deployments...
TXT
head -c 12000 notes.txt | sed '1s/^/[지침] 한국어, 불릿 7개, 명사형 간결 요약.\n[본문]\n/' | \
curl -s https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
  -d @- | jq -r '.output_text // .choices[0].message.content'

Ollama

head -c 12000 notes.txt | sed '1s/^/[지침] 한국어, 불릿 7개, 명사형 간결 요약.\n[본문]\n/' | \
curl -s http://localhost:11434/api/generate \
  -d '{"model":"llama2:chat","prompt": "'"$(cat -)"'"}' | jq -r '.response'

대용량/비텍스트(PDF 등)는 파일 업로드+참조 방식도 있습니다(Responses API + Files). OpenAI 플랫폼


4) 코드 설명 + 개선 제안

목적 : 코드 스니펫 해설 + 리팩터링 팁.
입력/출력 : example.py → “설명/복잡도/리팩터링” 3섹션.
가정 : 짧은 함수 단위.
테스트 : 파일 생성.

cat > example.py <<'PY'
def fib(n):
    if n <= 1: return n
    return fib(n-1)+fib(n-2)
PY
{
  echo "[지침] 한국어, 3섹션(설명/복잡도/리팩터링)."; 
  echo "[코드]"; cat example.py;
} | curl -s https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
  -d @- | jq -r '.output_text // .choices[0].message.content'

Ollama

{
  echo "[지침] 한국어, 3섹션(설명/복잡도/리팩터링)."; 
  echo "[코드]"; cat example.py;
} | curl -s http://localhost:11434/api/generate \
  -d '{"model":"codellama:7b-instruct","prompt": "'"$(cat -)"'"}' | jq -r '.response'

(Code Llama 계열 사용 예시와 프롬프트 팁 참고. Ollama+1)


5) git diff → 컨벤셔널 커밋 메시지

목적 : 변경점에서 커밋 메시지 자동 생성.
입력/출력 : git difffeat/fix/docs 형식 1~3줄.
가정 : 워킹 디렉토리에 git 저장소.
테스트 : 임의 수정 후 실행.

git diff | sed '1s/^/[지침] 한국어, Conventional Commits 스타일, 제목 72자 이내, 본문 1-2줄.\n[diff]\n/' | \
curl -s https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
  -d @- | jq -r '.output_text // .choices[0].message.content'

Ollama

git diff | sed '1s/^/[지침] 한국어, Conventional Commits 스타일, 제목 72자 이내, 본문 1-2줄.\n[diff]\n/' | \
curl -s http://localhost:11434/api/generate \
  -d '{"model":"mistral:instruct","prompt": "'"$(cat -)"'"}' | jq -r '.response'

6) 특정 파일에서 “할 일” 자동 추출

목적 : 문서/노트에서 TODO/액션아이템만 수집.
입력/출력 : 텍스트 → - [ ] Markdown 체크리스트.
가정 : 한국어 문서.
테스트 : meeting.md 생성.

cat > meeting.md <<'MD'
- 마이그레이션 일정 확정 필요
- 위험 항목 점검(롤백 플랜 작성)
MD
{ echo "[지침] 한국어, 문서에서 TODO를 '- [ ]' 목록으로만 추출."; cat meeting.md; } | \
curl -s https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
  -d @- | jq -r '.output_text // .choices[0].message.content'

Ollama

{ echo "[지침] 한국어, 문서에서 TODO를 '- [ ]' 목록으로만 추출."; cat meeting.md; } | \
curl -s http://localhost:11434/api/generate \
  -d '{"model":"llama2:chat","prompt": "'"$(cat -)"'"}' | jq -r '.response'

7) “문서 질의응답”(RAG-lite: 단일 파일)

목적 : 로컬 문서 내용에 대한 질의응답.
입력/출력 : 파일+질문 → 근거 포함 답변(줄번호/문장).
가정 : 1개 파일, 짧은 컨텍스트.
테스트 : policy.txt 생성.

cat > policy.txt <<'TXT'
SLA: critical fixes within 24 hours. Minor issues within 7 days.
TXT
{
  echo "[지침] 한국어, policy.txt 인용하여 답변, 근거 문장 인용 포함.";
  echo "[파일]"; cat policy.txt;
  echo "[질문] Minor 이슈 대응시간은?";
} | curl -s https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
  -d @- | jq -r '.output_text // .choices[0].message.content'

Ollama

{
  echo "[지침] 한국어, policy.txt 인용하여 답변, 근거 문장 인용 포함.";
  echo "[파일]"; cat policy.txt;
  echo "[질문] Minor 이슈 대응시간은?";
} | curl -s http://localhost:11434/api/generate \
  -d '{"model":"mistral:instruct","prompt": "'"$(cat -)"'"}' | jq -r '.response'

더 본격적인 툴 사용/웹검색 도구 연동은 Responses API의 “도구 사용” 가이드를 참고하세요. OpenAI 플랫폼


8) “설명 주석/Docstring 자동화”(파이썬 예)

목적 : 함수에 한국어 Docstring 부여.
입력/출력 : *.py → 패치 제안(“+++” 구문 포함).
가정 : 한 파일 단위.
테스트 : utils.py 생성.

cat > utils.py <<'PY'
def slugify(s):
    return s.lower().replace(' ','-')
PY
{
  echo "[지침] 한국어, PEP257 스타일 Docstring 삽입용 unified diff 출력.";
  echo "[파일]"; cat utils.py;
} | curl -s https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
  -d @- | jq -r '.output_text // .choices[0].message.content' | tee doc.patch

Ollama

{
  echo "[지침] 한국어, PEP257 스타일 Docstring 삽입용 unified diff 출력.";
  echo "[파일]"; cat utils.py;
} | curl -s http://localhost:11434/api/generate \
  -d '{"model":"codellama:7b-instruct","prompt": "'"$(cat -)"'"}' | jq -r '.response' | tee doc.patch

9) “텍스트 → JSON 분류” (간단 보장, jq 검증)

목적 : 임의 문장을 카테고리/긴급도 등으로 JSON 라벨링.
입력/출력 : 텍스트 → {"category":"...","priority":"..."}
가정 : 엄격 JSON이 필요하면 Structured Outputs 로 JSON Schema를 적용(아래 주석 링크).
테스트 : 3줄 입력.

cat > inbox.txt <<'TXT'
DB 연결 오류가 자주 발생함
문서 오탈자 수정 요청
신규 대시보드 제안
TXT

while IFS= read -r line; do
  printf '다음 문장을 JSON으로 분류해. {"category":("bug"|"docs"|"feature"),"priority":("high"|"low")}\n문장: %s\n' "$line" | \
  curl -s https://api.openai.com/v1/responses \
    -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
    -d @- | jq -re '.output_text // .choices[0].message.content | fromjson' || echo "⚠️ JSON 실패: $line"
done < inbox.txt

Ollama

while IFS= read -r line; do
  printf '다음 문장을 JSON으로 분류해. {"category":("bug"|"docs"|"feature"),"priority":("high"|"low")}\n문장: %s\n' "$line" | \
  curl -s http://localhost:11434/api/generate \
    -d '{"model":"mistral:instruct","prompt": "'"$(cat -)"'"}' | jq -re '.response | fromjson' || echo "⚠️ JSON 실패: $line"
done < inbox.txt

강한 JSON 보장 이 필요하면 Responses API의 Structured Outputs(JSON Schema 강제)을 사용하세요. OpenAI 플랫폼


10) “테스트케이스 요약 보고서”(코드블록만 모으기)

목적 : 로그/리포트에서 코드블록만 추출→핵심 요약.
입력/출력 : md/로그 → 실패 사례 위주 요약(불릿 5개).
가정 : 마크다운 로그.
테스트 : report.md 생성.

cat > report.md <<'MD'
### Case A

FAIL: timeout

### Case B

bash

OK

{  
echo "[지침] 한국어, 코드블록만 스캔→실패/원인/재현 단계를 불릿 5개로.";  
echo "[본문]"; cat report.md;  
} | curl -s https://api.openai.com/v1/responses   
-H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json"   
-d @- | jq -r '.output_text // .choices[0].message.content'

bash

Ollama

{
  echo "[지침] 한국어, 코드블록만 스캔→실패/원인/재현 단계를 불릿 5개로.";
  echo "[본문]"; cat report.md;
} | curl -s http://localhost:11434/api/generate \
  -d '{"model":"llama2:chat","prompt": "'"$(cat -)"'"}' | jq -r '.response'


스타일·운영 팁

  • 프롬프트 상단 고정 : [지침] 언어/톤/형식[본문][질문] 순서로 일관.

  • 온도/토큰 : 일관성 필요 시 온도 낮추기(모델 옵션: OpenAI temperature, Ollama options.temperature).

  • 길이 제어 : “최대 N줄/각 줄 M자” 등 명령을 프롬프트 첫 줄에 넣기.

  • 리트라이 : curl --max-time 60 || sleep 2 && 재시도 같은 간단한 재시도 루틴.

  • 도구/웹 검색 연동 : Responses API의 도구 사용(웹검색 등)으로 확장 가능. OpenAI 플랫폼+1


참고(공식 문서)

  • OpenAI Responses API : 엔드포인트/예시/모델 사용법. OpenAI 플랫폼+3

  • 파일 입력(업로드 후 참조) : 대용량 문서 처리. OpenAI 플랫폼

  • Structured Outputs(JSON Schema 강제) : 신뢰 가능한 JSON. OpenAI 플랫폼

  • Ollama 사용법/모델 라이브러리 : CLI·REST, CodeLlama/Mistral/Llama2 예시. Ollama+3

  • Ollama Windows 프리뷰(환경 참고). Ollama


필요하시면 위 스크립트를 하나의ai.sh 유틸(백엔드 스위치: OpenAI↔Ollama)로 묶어서 드릴게요. 원하는 작업군(번역/요약/코드/커밋 등) 중심으로 더 다듬어 최적화해드립니다.

python script 하나로 만들어줘. 몇 가지 잘 쓸만한 system prompt를 준비해놓고 예를 들어 script_name translate "안녕?" 이런 식으로 쓸 수 있게

좋아요! “한 파일짜리 파이썬 CLI”로 정리해 드릴게요. 아래는 바로 저장·실행 가능한 스크립트와 함께, (1) 전제 가정, (2) 재현 가능한 사용 예제(테스트), (3) 일관된 스타일(서브커맨드+프리셋)까지 갖춘 형태입니다.
요청하신 “script_name translate "안녕?"” 스타일 그대로 됩니다.


시작 전 체크리스트(개념)

  • 목적 고정 : translate, summarize, explain, commit, todo, docstring, classify 처럼 태스크 단일 목적.

  • 입력 경계 : --text, --file, 또는 표준입력(없으면 에러). 너무 길면 스스로 잘림 없음(원하면 head -c 등으로 전처리).

  • 출력 약속 : 텍스트 기본, classifyJSON 보장(가능 시 스키마 강제).

  • 백엔드 선택 : --backend openai|ollama(기본=openai). 모델은 --model로 오버라이드.

  • 보안/재현 : 키는 환경변수(OPENAI_API_KEY). Ollama는 로컬 11434 API 사용.


전제(가정)

  • OpenAI : 환경변수 OPENAI_API_KEY 필요. Python SDK 없이 HTTP /v1/responses 호출 사용(문서 기준 최신 기본 엔드포인트). OpenAI 플랫폼+1GitHub

  • Ollama : 로컬에서 ollama serve 동작, 기본 호스트 http://localhost:11434. /api/chat/api/generate 사용(분류 시 JSON 강제 위해 format:"json" 활용). OllamaGitHub


파일: ai_cli.py

아래를 그대로 저장하세요.

python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ai_cli.py — 단일 파이썬 스크립트로 여러 LLM 유틸 제공
- 서브커맨드: translate, summarize, explain, commit, todo, docstring, classify
- 백엔드: OpenAI Responses API(기본) 또는 Ollama 로컬 API
- 일관된 프롬프트 포맷: [system preset] + 사용자 입력
"""

import os, sys, json, argparse, subprocess, textwrap
from typing import Optional, Dict, Any
try:
    import requests
except Exception as e:
    print("requests 라이브러리가 필요합니다. `pip install requests` 후 다시 실행하세요.", file=sys.stderr)
    sys.exit(2)

# -----------------------------
# System prompt presets
# -----------------------------
PRESETS = {
    "translate": lambda to="en": (
        f"역할: 고품질 번역기\n"
        f"규칙:\n"
        f"- 목표 언어: {to}\n"
        f"- 의미와 뉘앙스를 보존하고, 자연스럽게 의역\n"
        f"- 출력은 번역문만(주석/설명 금지)\n"
    ),
    "summarize": (
        "역할: 전문 요약가\n"
        "규칙:\n"
        "- 한국어 출력, 불릿 7개 이내, 명사형 간결체\n"
        "- 핵심 주장/근거/위험/다음 액션 우선\n"
    ),
    "explain": (
        "역할: 코드 리뷰어\n"
        "규칙:\n"
        "- 한국어로 3섹션 출력: [설명] [복잡도] [리팩터링]\n"
        "- 핵심 로직, 시간/공간 복잡도, 구체적 개선안 포함\n"
    ),
    "commit": (
        "역할: Conventional Commits 작성기\n"
        "규칙:\n"
        "- 한국어, 제목 72자 이내, 한 줄 요약 + 1~2줄 본문\n"
        "- 타입은 feat/fix/docs/refactor/test/chore 중 선택\n"
        "- 변경 의도와 영향 범위를 간결히\n"
        "- 출력은 커밋 메시지 본문만\n"
    ),
    "todo": (
        "역할: TODO 추출기\n"
        "규칙:\n"
        "- 한국어, 입력 문서에서 실행 가능한 액션만 추려서 '- [ ] ' 체크리스트로만 출력\n"
    ),
    "docstring": (
        "역할: 파이썬 Docstring 작성기\n"
        "규칙:\n"
        "- 한국어 PEP257 스타일\n"
        "- 수정 제안은 unified diff 패치 형식으로 출력(+++ 파일명 포함)\n"
    ),
    "classify": (
        "역할: 간단 이슈 분류기\n"
        "규칙:\n"
        "- 입력 한 줄을 카테고리와 우선순위로 JSON 출력\n"
        "- category ∈ {\"bug\",\"docs\",\"feature\"}, priority ∈ {\"high\",\"low\"}\n"
        "- 출력은 하나의 순수 JSON 객체만\n"
    ),
}

# JSON Schema for classify (OpenAI Structured Outputs 용)
CLASSIFY_JSON_SCHEMA = {
    "name": "classification",
    "strict": True,
    "schema": {
        "type": "object",
        "properties": {
            "category": {"type": "string", "enum": ["bug", "docs", "feature"]},
            "priority": {"type": "string", "enum": ["high", "low"]},
        },
        "required": ["category", "priority"],
        "additionalProperties": False,
    },
}

# -----------------------------
# I/O helpers
# -----------------------------
def read_input_text(args) -> str:
    if args.text:
        return args.text
    if args.file:
        with open(args.file, "r", encoding="utf-8") as f:
            return f.read()
    if not sys.stdin.isatty():
        return sys.stdin.read()
    print("입력이 없습니다. --text, --file 또는 표준입력으로 내용을 전달하세요.", file=sys.stderr)
    sys.exit(1)

def maybe_git_diff(staged: bool) -> str:
    cmd = ["git", "diff", "--staged"] if staged else ["git", "diff"]
    try:
        out = subprocess.check_output(cmd, encoding="utf-8", stderr=subprocess.STDOUT)
        if not out.strip():
            print("git diff 결과가 비어 있습니다. 변경사항을 확인하세요.", file=sys.stderr)
            sys.exit(1)
        return out
    except subprocess.CalledProcessError as e:
        print("git diff 실행 실패:", e.output, file=sys.stderr)
        sys.exit(1)

# -----------------------------
# Backends
# -----------------------------
def call_openai(system_text: str, user_text: str, model: str, *, json_schema: Optional[Dict[str, Any]] = None) -> str:
    api_key = os.environ.get("OPENAI_API_KEY")
    if not api_key:
        print("환경변수 OPENAI_API_KEY 가 필요합니다.", file=sys.stderr)
        sys.exit(2)

    url = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1/responses")
    headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}

    payload: Dict[str, Any] = {
        "model": model,
        "instructions": system_text,
        "input": user_text,
    }
    if json_schema:
        payload["response_format"] = {
            "type": "json_schema",
            "json_schema": json_schema,
        }

    resp = requests.post(url, headers=headers, data=json.dumps(payload), timeout=120)
    if resp.status_code != 200:
        print(f"[OpenAI] HTTP {resp.status_code}: {resp.text}", file=sys.stderr)
        sys.exit(3)
    data = resp.json()
    # Responses API는 output_text에 전체 텍스트가 평탄화되어 제공됨
    text = data.get("output_text")
    if not text:
        # 호환용(혹시 구조가 바뀐 경우)
        try:
            text = data["choices"][0]["message"]["content"]
            if isinstance(text, list):  # 멀티파트 대비
                text = "".join(part.get("text", "") for part in text if isinstance(part, dict))
        except Exception:
            text = json.dumps(data, ensure_ascii=False)
    return text

def call_ollama_chat(system_text: str, user_text: str, model: str) -> str:
    host = os.environ.get("OLLAMA_HOST", "http://localhost:11434")
    url = f"{host}/api/chat"
    payload = {
        "model": model,
        "messages": [
            {"role": "system", "content": system_text},
            {"role": "user", "content": user_text},
        ],
        "stream": False,
    }
    resp = requests.post(url, data=json.dumps(payload), timeout=120)
    if resp.status_code != 200:
        print(f"[Ollama chat] HTTP {resp.status_code}: {resp.text}", file=sys.stderr)
        sys.exit(3)
    data = resp.json()
    # Ollama chat 응답 형태: {"message":{"content": "..."}}
    try:
        return data["message"]["content"]
    except Exception:
        # generate 형식 등 다양성 대비
        return data.get("response", json.dumps(data, ensure_ascii=False))

def call_ollama_generate(prompt_text: str, model: str, *, json_mode: bool = False) -> str:
    host = os.environ.get("OLLAMA_HOST", "http://localhost:11434")
    url = f"{host}/api/generate"
    payload = {
        "model": model,
        "prompt": prompt_text,
        "stream": False,
    }
    if json_mode:
        payload["format"] = "json"
    resp = requests.post(url, data=json.dumps(payload), timeout=120)
    if resp.status_code != 200:
        print(f"[Ollama gen] HTTP {resp.status_code}: {resp.text}", file=sys.stderr)
        sys.exit(3)
    data = resp.json()
    return data.get("response", json.dumps(data, ensure_ascii=False))

# -----------------------------
# Router
# -----------------------------
def run_task(args):
    backend = args.backend
    model = args.model

    def run(system_text: str, user_text: str, *, json_schema: Optional[Dict[str, Any]] = None, force_json=False) -> str:
        if backend == "openai":
            return call_openai(system_text, user_text, model, json_schema=json_schema)
        # Ollama: JSON 보장이 필요하면 generate + json_mode 사용
        if force_json:
            prompt = f"{system_text.strip()}\n\n[입력]\n{user_text.strip()}\n"
            return call_ollama_generate(prompt, model, json_mode=True)
        return call_ollama_chat(system_text, user_text, model)

    # translate
    if args.command == "translate":
        to_lang = args.to
        sys_prompt = PRESETS["translate"](to=to_lang)
        text = read_input_text(args)
        print(run(sys_prompt, text).strip())
        return

    # summarize
    if args.command == "summarize":
        sys_prompt = PRESETS["summarize"]
        text = read_input_text(args)
        print(run(sys_prompt, text).strip())
        return

    # explain (code)
    if args.command == "explain":
        sys_prompt = PRESETS["explain"]
        text = read_input_text(args)
        print(run(sys_prompt, text).strip())
        return

    # commit (from text or git diff)
    if args.command == "commit":
        sys_prompt = PRESETS["commit"]
        if args.git or args.staged:
            text = maybe_git_diff(staged=args.staged)
        else:
            text = read_input_text(args)
        user_msg = f"[변경점]\n{text}"
        print(run(sys_prompt, user_msg).strip())
        return

    # todo extraction
    if args.command == "todo":
        sys_prompt = PRESETS["todo"]
        text = read_input_text(args)
        print(run(sys_prompt, text).strip())
        return

    # docstring (unified diff patch)
    if args.command == "docstring":
        sys_prompt = PRESETS["docstring"]
        text = read_input_text(args)
        user_msg = f"[파일]\n{text}"
        print(run(sys_prompt, user_msg).strip())
        return

    # classify -> strict JSON
    if args.command == "classify":
        sys_prompt = PRESETS["classify"]
        # 한 줄만 분류(여러 줄은 쉘에서 while read 루프 권장)
        text = read_input_text(args).strip()
        if backend == "openai":
            out = run(sys_prompt, text, json_schema=CLASSIFY_JSON_SCHEMA)
        else:
            # Ollama: JSON 강제는 generate/format=json 사용
            prompt = textwrap.dedent(f"""
            {sys_prompt}
            출력은 JSON 한 객체만. 예: category
            문장: {text}
            """).strip()
            out = call_ollama_generate(prompt, model, json_mode=True)
        # pretty print or raw
        try:
            obj = json.loads(out)
            print(json.dumps(obj, ensure_ascii=False))
        except Exception:
            # 혹시 순수 JSON이 아니면 원본 출력
            print(out.strip())
        return

    # unknown
    print("지원하지 않는 커맨드입니다.", file=sys.stderr)
    sys.exit(1)

# -----------------------------
# CLI
# -----------------------------
def build_parser():
    p = argparse.ArgumentParser(description="LLM one-file CLI (OpenAI / Ollama)")
    p.add_argument("--backend", choices=["openai", "ollama"], default=os.environ.get("AI_BACKEND", "openai"),
                   help="백엔드 선택 (기본: openai)")
    p.add_argument("--model", default=os.environ.get("AI_MODEL", None),
                   help="사용할 모델명 (OpenAI 예: gpt-4o-mini, Ollama 예: llama3.2)")
    sub = p.add_subparsers(dest="command", required=True)

    def common(sp):
        sp.add_argument("--text", help="직접 입력 텍스트")
        sp.add_argument("--file", help="입력 파일 경로")

    sp = sub.add_parser("translate", help="번역")
    common(sp)
    sp.add_argument("--to", default="en", help="목표 언어 코드(기본=en)")

    sp = sub.add_parser("summarize", help="요약(불릿 7)")
    common(sp)

    sp = sub.add_parser("explain", help="코드 설명+개선")
    common(sp)

    sp = sub.add_parser("commit", help="Conventional Commits 생성")
    common(sp)
    sp.add_argument("--git", action="store_true", help="git diff 사용")
    sp.add_argument("--staged", action="store_true", help="git diff --staged 사용")

    sp = sub.add_parser("todo", help="문서에서 TODO 추출")
    common(sp)

    sp = sub.add_parser("docstring", help="파이썬 파일에 대한 Docstring 패치 제안")
    common(sp)

    sp = sub.add_parser("classify", help="문장을 JSON으로 라벨링(category/priority)")
    common(sp)

    return p

def main():
    parser = build_parser()
    args = parser.parse_args()

    # 기본 모델 값 보정
    if not args.model:
        args.model = "gpt-4o-mini" if args.backend == "openai" else "llama3.2"

    run_task(args)

if __name__ == "__main__":
    main()


사용 예시(재현 가능한 테스트)

아래 명령은 Mac/Linux 기준. Windows는 python 대신 py 또는 python.exe 사용하세요.

  1. 설치 & 실행 준비 ``` bash

pip install requests export OPENAI_API_KEY="sk-…" # OpenAI 백엔드 쓸 때만

(Ollama 사용 시) ollama serve & 모델 준비: ollama pull llama3.2

  2. 번역 (요청 예시와 동일)

bash

python ai_cli.py translate –text "안녕?" –to en

또는 파일/STDIN:

echo "PostgreSQL 인덱스는…" | python ai_cli.py translate –to en

  3. 요약

bash

python ai_cli.py summarize –text "회의에서 마이그레이션 일정과 위험…"

  4. 코드 설명

bash

cat > example.py «'PY' def fib(n): if n <= 1: return n return fib(n-1)+fib(n-2) PY python ai_cli.py explain –file example.py

  5. 커밋 메시지(로컬 변경에서)

bash

워킹 디렉토리에서

python ai_cli.py commit –git # git diff 사용 python ai_cli.py commit –staged # git diff –staged 사용

  6. TODO 추출

bash

python ai_cli.py todo –text "위험 항목 점검 및 롤백 플랜 작성 필요"

  7. 분류(JSON 보장)

bash

python ai_cli.py classify –text "DB 연결 오류가 자주 발생함"

출력 예: {"category":"bug","priority":"high"}

  8. Docstring 패치 제안

bash

python ai_cli.py docstring –file example.py

unified diff 형태로 나옵니다(수동 패치 or git apply 가능).

* * *

## 입출력 요약(서브커맨드별)

  * `translate`: 입력=임의 텍스트 → 출력=목표 언어 번역문 1개.

  * `summarize`: 입력=문서 텍스트 → 출력=불릿 ≤7개 핵심 요약(한글).

  * `explain`: 입력=코드 → 출력=[설명][복잡도][리팩터링] 3섹션.

  * `commit`: 입력=git diff/텍스트 → 출력=Conventional Commits 메시지 본문.

  * `todo`: 입력=문서 → 출력=Markdown 체크리스트(`- [ ] ...`).

  * `docstring`: 입력=파이썬 파일 → 출력=unified diff 패치.

  * `classify`: 입력=한 줄 문장 → 출력=JSON `{category, priority}` (OpenAI는 **구조적 출력** 사용, Ollama는 `format:"json"`). [OpenAI 플랫폼](https://platform.openai.com/docs/guides/structured-outputs?utm_source=chatgpt.com)[Ollama](https://ollama.readthedocs.io/en/api/?utm_source=chatgpt.com)

* * *

## 구현 메모(일관된 스타일)

  * 프롬프트는 **시스템 프리셋 + 사용자 입력** 규격화.

  * OpenAI는 **Responses API** 를 사용해 `instructions + input` 형태로 호출. [GitHub](https://github.com/openai/openai-python?utm_source=chatgpt.com)[OpenAI 플랫폼](https://platform.openai.com/docs/api-reference/responses?utm_source=chatgpt.com)

  * `classify`는 OpenAI에서 **Structured Outputs(JSON Schema)** 를 사용하도록 `response_format`을 설정. [OpenAI 플랫폼](https://platform.openai.com/docs/guides/structured-outputs?utm_source=chatgpt.com)[OpenAI](https://openai.com/index/introducing-structured-outputs-in-the-api/?utm_source=chatgpt.com)

  * Ollama는 일반 태스크는 `/api/chat`, JSON 강제 태스크는 `/api/generate + format:"json"` 사용. [Ollama](https://ollama.readthedocs.io/en/api/?utm_source=chatgpt.com)

* * *

## 참고

  * **OpenAI Responses API** 개요/빠른 시작: [OpenAI 플랫폼+1](https://platform.openai.com/docs/quickstart?utm_source=chatgpt.com)

  * **OpenAI Python 라이브러리 README(Responses 예시)** : [GitHub](https://github.com/openai/openai-python)

  * **Structured Outputs(JSON 스키마 강제)** : [OpenAI 플랫폼](https://platform.openai.com/docs/guides/structured-outputs?utm_source=chatgpt.com)[OpenAI](https://openai.com/index/introducing-structured-outputs-in-the-api/?utm_source=chatgpt.com)

  * **Ollama API(chat/generate, JSON 모드)** : [Ollama](https://ollama.readthedocs.io/en/api/?utm_source=chatgpt.com)[GitHub](https://github.com/ollama/ollama?utm_source=chatgpt.com)

필요하시면 `--stream` 옵션, 배치 처리(파일 한 줄씩 분류), 프리셋 추가(예: `ragfile`, `rewrite`, `polish`)도 확장해 드릴게요.

<p class="chat--user" markdown="1">

1\. 이 스크립트 이름을 10개 정도 작명해줘. (개인적인 의견으로는 간단한 ai라는 느낌이 들면 좋을 듯) 2\. 이걸 python project화 할거야. sturcture와 folder 구조, 각 파일을 작성해줘. 3\. 그리고 새로운 subcommand를 좀 쉽게 확장성 있게 등록할 수 있게 구조를 작성해줘. 새로운 subcommand를 추가하는 방법 예시도 추가해줘. 4\. ENPOINT 주소도 내가 등록할 수 있도록 해줘.

</p>

좋아요—원하신 “간단한 ai 느낌”의 이름 제안부터, **프로젝트 구조/파일 전체** , **확장 가능한 서브커맨드 구조** , **엔드포인트(ENDPOINT) 설정 방식** 까지 한 번에 정리합니다.  
(요청하신 대로 단계별로 생각을 드러내며, 전제·재현 예시·일관 스타일을 포함합니다.)

* * *

# 0) 한눈에 계획 (step-by-step)

  1. **이름 후보 10개** 를 먼저 정리

  2. **Python 패키지 구조** 를 제시(콘솔 엔트리포인트 포함)

  3. **확장성 핵심** : `CommandRegistry` \+ `commands/` 자동 로드 + **entry points 플러그인** 지원

  4. **ENDPOINT 설정** : CLI 옵션, 환경변수, YAML 설정 파일(우선순위: CLI > ENV > YAML 기본값)

  5. **재현 예시** : 설치·실행 커맨드, 새 서브커맨드 추가 예시 포함

* * *

# 1) 스크립트/프로젝트 이름 제안 (10)

간단·가벼운 “ai 느낌” 위주로 골랐어요.

  1. `simpleai`

  2. `nan.ai` (`nanai` 명령)

  3. `aikit`

  4. `mini-ai` (`miniai`)

  5. `ai-do`

  6. `aihand`

  7. `snackai`

  8. `clai` (command-line-ai)

  9. `aiplay`

  10. `tinyai`

> 아래 예시는 패키지명을 **`simpleai`** , 콘솔 명령을 **`simpleai`**로 가정합니다. (원하시면 다른 이름으로 일괄 치환 가능)

* * *

# 2) 프로젝트 구조 & 전체 파일

## 디렉토리 구조

graphql

simpleai/ ├─ simpleai/ │ ├─ init.py │ ├─ cli.py # 콘솔 엔트리포인트 │ ├─ config.py # CLI/ENV/YAML 설정 로더 │ ├─ presets.py # system prompts 프리셋 │ ├─ registry.py # CommandRegistry & 자동 로드/플러그인 │ ├─ utils.py # 공통 I/O 유틸(입력읽기, git diff 등) │ ├─ backends/ │ │ ├─ init.py │ │ ├─ openai_backend.py # OpenAI Responses API 호출 │ │ └─ ollama_backend.py # Ollama chat/generate 호출 │ └─ commands/ │ ├─ init.py # 모듈 자동등록 트리거 │ ├─ translate.py │ ├─ summarize.py │ ├─ explain.py │ ├─ commit.py │ ├─ todo.py │ ├─ docstring.py │ └─ classify.py ├─ examples/ │ ├─ example.py │ ├─ notes.txt │ └─ policy.txt ├─ tests/ │ └─ test_smoke.py ├─ pyproject.toml ├─ README.md └─ .env.example

> 의존성: `requests` (가벼움 유지), 표준 `argparse`. YAML 읽기는 선택(Optional)—없으면 ENV/CLI만으로 동작.

* * *

## 파일: `simpleai/__init__.py`

python

all = ["cli"] version = "0.1.0"

## 파일: `simpleai/presets.py`

python

def preset_translate(to="en") -> str: return ( "역할: 고품질 번역기\n" f"- 목표 언어: {to}\n" "- 의미/뉘앙스 보존, 자연스러운 의역\n" "- 출력은 번역문만(주석 금지)\n" )

PRESETS = { "summarize": ( "역할: 전문 요약가\n" "- 한국어 출력, 불릿 최대 7개, 명사형 간결체\n" "- 핵심 주장/근거/위험/다음 액션 우선\n" ), "explain": ( "역할: 코드 리뷰어\n" "- 한국어 3섹션: [설명] [복잡도] [리팩터링]\n" "- 핵심 로직, 시간/공간 복잡도, 개선안\n" ), "commit": ( "역할: Conventional Commits 작성기\n" "- 한국어, 제목 72자 이내 + 1~2줄 본문\n" "- feat/fix/docs/refactor/test/chore 중 선택\n" "- 출력은 메시지 본문만\n" ), "todo": ( "역할: TODO 추출기\n" "- 입력에서 실행 가능한 액션만 '- [ ] ' 체크리스트로 출력\n" ), "docstring": ( "역할: 파이썬 Docstring 작성기\n" "- 한국어 PEP257 스타일\n" "- unified diff 패치 형식(+++ 파일명 포함)\n" ), "classify": ( "역할: 간단 이슈 분류기\n" "- 입력 한 줄 → JSON {category, priority}\n" '- category ∈ {"bug","docs","feature"}, priority ∈ {"high","low"}\n' "- 출력은 순수 JSON 한 객체만\n" ), }

## 파일: `simpleai/config.py`

python

import os, json, pathlib from dataclasses import dataclass from typing import Optional

try: import yaml # optional except Exception: yaml = None

DEFAULT_CONFIG_PATHS = [ "~/.simpleai/config.yaml", "./.simpleai/config.yaml", ]

@dataclass class OpenAIConfig: api_key: Optional[str] = None base_url: str = "https://api.openai.com/v1" responses_path: str = "/responses" model: str = "gpt-4o-mini"

@dataclass class OllamaConfig: host: str = "http://localhost:11434" # ex) http://127.0.0.1:11434 chat_path: str = "/api/chat" generate_path: str = "/api/generate" model: str = "llama3.2"

@dataclass class AppConfig: backend: str = "openai" # or "ollama" openai: OpenAIConfig = OpenAIConfig() ollama: OllamaConfig = OllamaConfig()

def _load_yaml_dict() -> dict: data = {} for p in DEFAULT_CONFIG_PATHS: path = pathlib.Path(os.path.expanduser(p)) if path.exists() and path.is_file(): if yaml is None: continue with path.open("r", encoding="utf-8") as f: y = yaml.safe_load(f) or {} data.update(y) return data

def load_config(cli_args) -> AppConfig: # base from YAML y = _load_yaml_dict()

cfg = AppConfig(
    backend = getattr(cli_args, "backend", None) or os.getenv("SIMPLEAI_BACKEND", y.get("defaults", {}).get("backend", "openai")),
    openai = OpenAIConfig(
        api_key = os.getenv("OPENAI_API_KEY", y.get("openai", {}).get("api_key")),
        base_url = getattr(cli_args, "openai_base_url", None) or os.getenv("SIMPLEAI_OPENAI_BASE_URL", y.get("openai", {}).get("base_url", "https://api.openai.com/v1")),
        responses_path = getattr(cli_args, "openai_responses_path", None) or os.getenv("SIMPLEAI_OPENAI_RESPONSES_PATH", y.get("openai", {}).get("responses_path", "/responses")),
        model = getattr(cli_args, "model", None) or os.getenv("OPENAI_MODEL", y.get("openai", {}).get("model", "gpt-4o-mini")),
    ),
    ollama = OllamaConfig(
        host = getattr(cli_args, "ollama_host", None) or os.getenv("SIMPLEAI_OLLAMA_HOST", y.get("ollama", {}).get("host", "http://localhost:11434")),
        chat_path = os.getenv("SIMPLEAI_OLLAMA_CHAT_PATH", y.get("ollama", {}).get("chat_path", "/api/chat")),
        generate_path = os.getenv("SIMPLEAI_OLLAMA_GENERATE_PATH", y.get("ollama", {}).get("generate_path", "/api/generate")),
        model = getattr(cli_args, "model", None) or os.getenv("OLLAMA_MODEL", y.get("ollama", {}).get("model", "llama3.2")),
    ),
)
return cfg
## 파일: `simpleai/utils.py`

python

import sys, subprocess

def read_input_text(args) -> str: if getattr(args, "text", None): return args.text if getattr(args, "file", None): with open(args.file, "r", encoding="utf-8") as f: return f.read() if not sys.stdin.isatty(): return sys.stdin.read() raise SystemExit("입력이 없습니다. –text, –file 또는 표준입력으로 내용을 전달하세요.")

def git_diff(staged: bool=False) -> str: cmd = ["git", "diff", "–staged"] if staged else ["git", "diff"] out = subprocess.check_output(cmd, encoding="utf-8", stderr=subprocess.STDOUT) if not out.strip(): raise SystemExit("git diff 결과가 비어 있습니다.") return out

## 파일: `simpleai/backends/openai_backend.py`

python

import json, requests

def call_openai(instructions: str, user_text: str, *, base_url: str, responses_path: str, api_key: str, model: str, response_format: dict | None = None) -> str: if not api_key: raise SystemExit("환경변수 OPENAI_API_KEY 또는 설정 파일이 필요합니다.")

url = base_url.rstrip("/") + responses_path
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
payload = {"model": model, "instructions": instructions, "input": user_text}
if response_format:
    payload["response_format"] = response_format

resp = requests.post(url, headers=headers, data=json.dumps(payload), timeout=120)
if resp.status_code != 200:
    raise SystemExit(f"[OpenAI] HTTP {resp.status_code}: {resp.text}")

data = resp.json()
text = data.get("output_text")
if not text:
    # 호환용
    try:
        c = data["choices"][0]["message"]["content"]
        text = "".join(p.get("text","") for p in c if isinstance(p, dict)) if isinstance(c, list) else c
    except Exception:
        text = json.dumps(data, ensure_ascii=False)
return text.strip()
## 파일: `simpleai/backends/ollama_backend.py`

python

import json, requests

def _post(url, payload): r = requests.post(url, data=json.dumps(payload), timeout=120) if r.status_code != 200: raise SystemExit(f"[Ollama] HTTP {r.status_code}: {r.text}") return r.json()

def chat(instructions: str, user_text: str, *, host: str, chat_path: str, model: str) -> str: url = host.rstrip("/") + chat_path payload = { "model": model, "messages": [ {"role": "system", "content": instructions}, {"role": "user", "content": user_text}, ], "stream": False, } data = _post(url, payload) return (data.get("message", {}) or {}).get("content") or data.get("response","")

def generate(prompt: str, *, host: str, generate_path: str, model: str, json_mode: bool=False) -> str: url = host.rstrip("/") + generate_path payload = {"model": model, "prompt": prompt, "stream": False} if json_mode: payload["format"] = "json" data = _post(url, payload) return data.get("response","")

## 파일: `simpleai/registry.py`

python

import argparse, importlib, pkgutil from typing import Callable, Dict, List, Optional from importlib import metadata

Handler = Callable[[argparse.Namespace, "AppDeps"], None] Adder = Callable[[argparse._SubParsersAction], argparse.ArgumentParser]

class CommandRegistry: def init(self): self._commands: Dict[str, Dict[str, object]] = {}

def register(self, name: str, add_arguments: Adder, handler: Handler):
    if name in self._commands:
        raise ValueError(f"duplicate command: {name}")
    self._commands[name] = {"add_arguments": add_arguments, "handler": handler}

def build_subparsers(self, sub: argparse._SubParsersAction):
    for name, spec in self._commands.items():
        sp = spec["add_arguments"](sub)
        sp.set_defaults(_handler=spec["handler"])

def names(self) -> List[str]:
    return sorted(self._commands.keys())

def autoload_builtin_commands(registry: CommandRegistry): # simpleai.commands.* 모듈 자동 import → 각 모듈에서 register(registry) import simpleai.commands as pkg for mod in pkgutil.iter_modules(pkg.path, pkg.name + "."): importlib.import_module(mod.name)

def autoload_plugin_entrypoints(registry: CommandRegistry): # 외부 패키지가 entry_points simpleai.commands에 등록한 register 함수를 호출 try: eps = metadata.entry_points() group = eps.select(group="simpleai.commands") except Exception: group = [] for ep in group: # ep.name 은 커맨드명(권장), ep.load() → callable(register) register_fn = ep.load() register_fn(registry)

의존성 컨테이너 (백엔드/프리셋/설정 접근)

class AppDeps: def init(self, cfg, openai, ollama, presets): self.cfg = cfg self.openai = openai self.ollama = ollama self.presets = presets

REGISTRY = CommandRegistry()

## 파일: `simpleai/commands/__init__.py`

python

각 모듈에서 import 시 simpleai.registry.REGISTRY.register(…) 호출

## 파일: `simpleai/commands/translate.py`

python

import argparse from simpleai.registry import REGISTRY, AppDeps from simpleai.utils import read_input_text from simpleai.presets import preset_translate

def _add(sub): sp = sub.add_parser("translate", help="번역") sp.add_argument("–text") sp.add_argument("–file") sp.add_argument("–to", default="en", help="목표 언어 코드(기본=en)") return sp

def _handle(args: argparse.Namespace, deps: AppDeps): sys_prompt = preset_translate(to=args.to) text = read_input_text(args) if deps.cfg.backend == "openai": out = deps.openai( instructions=sys_prompt, user_text=text, base_url=deps.cfg.openai.base_url, responses_path=deps.cfg.openai.responses_path, api_key=deps.cfg.openai.api_key, model=deps.cfg.openai.model, ) else: out = deps.ollama.chat( instructions=sys_prompt, user_text=text, host=deps.cfg.ollama.host, chat_path=deps.cfg.ollama.chat_path, model=deps.cfg.ollama.model, ) print(out)

def register(registry=REGISTRY): registry.register("translate", _add, _handle)

register()

## 파일: `simpleai/commands/summarize.py`

python

import argparse from simpleai.registry import REGISTRY, AppDeps from simpleai.utils import read_input_text from simpleai.presets import PRESETS

def _add(sub): sp = sub.add_parser("summarize", help="요약(불릿 7)") sp.add_argument("–text") sp.add_argument("–file") return sp

def _handle(args: argparse.Namespace, deps: AppDeps): sys_prompt = PRESETS["summarize"] text = read_input_text(args) if deps.cfg.backend == "openai": out = deps.openai( instructions=sys_prompt, user_text=text, base_url=deps.cfg.openai.base_url, responses_path=deps.cfg.openai.responses_path, api_key=deps.cfg.openai.api_key, model=deps.cfg.openai.model, ) else: out = deps.ollama.chat( instructions=sys_prompt, user_text=text, host=deps.cfg.ollama.host, chat_path=deps.cfg.ollama.chat_path, model=deps.cfg.ollama.model, ) print(out)

def register(registry=REGISTRY): registry.register("summarize", _add, _handle)

register()

## 파일: `simpleai/commands/explain.py`

python

import argparse from simpleai.registry import REGISTRY, AppDeps from simpleai.utils import read_input_text from simpleai.presets import PRESETS

def _add(sub): sp = sub.add_parser("explain", help="코드 설명+개선") sp.add_argument("–text") sp.add_argument("–file") return sp

def _handle(args: argparse.Namespace, deps: AppDeps): sys_prompt = PRESETS["explain"] text = read_input_text(args) if deps.cfg.backend == "openai": out = deps.openai( instructions=sys_prompt, user_text=text, base_url=deps.cfg.openai.base_url, responses_path=deps.cfg.openai.responses_path, api_key=deps.cfg.openai.api_key, model=deps.cfg.openai.model, ) else: out = deps.ollama.chat( instructions=sys_prompt, user_text=text, host=deps.cfg.ollama.host, chat_path=deps.cfg.ollama.chat_path, model=deps.cfg.ollama.model, ) print(out)

def register(registry=REGISTRY): registry.register("explain", _add, _handle)

register()

## 파일: `simpleai/commands/commit.py`

python

import argparse from simpleai.registry import REGISTRY, AppDeps from simpleai.utils import read_input_text, git_diff from simpleai.presets import PRESETS

def _add(sub): sp = sub.add_parser("commit", help="Conventional Commits 생성") sp.add_argument("–text") sp.add_argument("–file") sp.add_argument("–git", action="store_true") sp.add_argument("–staged", action="store_true") return sp

def _handle(args: argparse.Namespace, deps: AppDeps): sys_prompt = PRESETS["commit"] if args.git or args.staged: text = git_diff(staged=args.staged) else: text = read_input_text(args) user_msg = f"[변경점]\n{text}" if deps.cfg.backend == "openai": out = deps.openai( instructions=sys_prompt, user_text=user_msg, base_url=deps.cfg.openai.base_url, responses_path=deps.cfg.openai.responses_path, api_key=deps.cfg.openai.api_key, model=deps.cfg.openai.model, ) else: out = deps.ollama.chat( instructions=sys_prompt, user_text=user_msg, host=deps.cfg.ollama.host, chat_path=deps.cfg.ollama.chat_path, model=deps.cfg.ollama.model, ) print(out)

def register(registry=REGISTRY): registry.register("commit", _add, _handle)

register()

## 파일: `simpleai/commands/todo.py`

python

import argparse from simpleai.registry import REGISTRY, AppDeps from simpleai.utils import read_input_text from simpleai.presets import PRESETS

def _add(sub): sp = sub.add_parser("todo", help="문서에서 TODO 추출") sp.add_argument("–text") sp.add_argument("–file") return sp

def _handle(args: argparse.Namespace, deps: AppDeps): sys_prompt = PRESETS["todo"] text = read_input_text(args) if deps.cfg.backend == "openai": out = deps.openai( instructions=sys_prompt, user_text=text, base_url=deps.cfg.openai.base_url, responses_path=deps.cfg.openai.responses_path, api_key=deps.cfg.openai.api_key, model=deps.cfg.openai.model, ) else: out = deps.ollama.chat( instructions=sys_prompt, user_text=text, host=deps.cfg.ollama.host, chat_path=deps.cfg.ollama.chat_path, model=deps.cfg.ollama.model, ) print(out)

def register(registry=REGISTRY): registry.register("todo", _add, _handle)

register()

## 파일: `simpleai/commands/docstring.py`

python

import argparse from simpleai.registry import REGISTRY, AppDeps from simpleai.utils import read_input_text from simpleai.presets import PRESETS

def _add(sub): sp = sub.add_parser("docstring", help="파이썬 파일 Docstring 패치 제안") sp.add_argument("–text") sp.add_argument("–file") return sp

def _handle(args: argparse.Namespace, deps: AppDeps): sys_prompt = PRESETS["docstring"] text = read_input_text(args) user_msg = f"[파일]\n{text}" if deps.cfg.backend == "openai": out = deps.openai( instructions=sys_prompt, user_text=user_msg, base_url=deps.cfg.openai.base_url, responses_path=deps.cfg.openai.responses_path, api_key=deps.cfg.openai.api_key, model=deps.cfg.openai.model, ) else: out = deps.ollama.chat( instructions=sys_prompt, user_text=user_msg, host=deps.cfg.ollama.host, chat_path=deps.cfg.ollama.chat_path, model=deps.cfg.ollama.model, ) print(out)

def register(registry=REGISTRY): registry.register("docstring", _add, _handle)

register()

## 파일: `simpleai/commands/classify.py`

python

import argparse, json, textwrap from simpleai.registry import REGISTRY, AppDeps from simpleai.utils import read_input_text from simpleai.presets import PRESETS

CLASSIFY_SCHEMA = { "name": "classification", "strict": True, "schema": { "type": "object", "properties": { "category": {"type": "string", "enum": ["bug","docs","feature"]}, "priority": {"type": "string", "enum": ["high","low"]}, }, "required": ["category","priority"], "additionalProperties": False, }, }

def _add(sub): sp = sub.add_parser("classify", help="문장을 JSON으로 라벨링") sp.add_argument("–text") sp.add_argument("–file") return sp

def _handle(args: argparse.Namespace, deps: AppDeps): sys_prompt = PRESETS["classify"] text = read_input_text(args).strip()

if deps.cfg.backend == "openai":
    out = deps.openai(
        instructions=sys_prompt, user_text=text,
        base_url=deps.cfg.openai.base_url,
        responses_path=deps.cfg.openai.responses_path,
        api_key=deps.cfg.openai.api_key,
        model=deps.cfg.openai.model,
        response_format={"type":"json_schema","json_schema":CLASSIFY_SCHEMA},
    )
    raw = out
else:
    prompt = textwrap.dedent(f"""
    {sys_prompt}
    출력은 JSON 한 객체만. 예: category
    문장: {text}
    """).strip()
    raw = deps.ollama.generate(
        prompt, host=deps.cfg.ollama.host, generate_path=deps.cfg.ollama.generate_path,
        model=deps.cfg.ollama.model, json_mode=True
    )

try:
    obj = json.loads(raw)
    print(json.dumps(obj, ensure_ascii=False))
except Exception:
    print(raw)

def register(registry=REGISTRY): registry.register("classify", _add, _handle)

register()

## 파일: `simpleai/cli.py`

python

#!/usr/bin/env python3 import argparse from simpleai.registry import REGISTRY, autoload_builtin_commands, autoload_plugin_entrypoints, AppDeps from simpleai.config import load_config from simpleai.backends import openai_backend, ollama_backend from simpleai import presets as presets_mod

def build_parser(): p = argparse.ArgumentParser(prog="simpleai", description="Simple AI CLI") p.add_argument("–backend", choices=["openai","ollama"], help="백엔드 선택") p.add_argument("–model", help="모델명(백엔드별 기본값 존재)") # ENDPOINT 설정(요청사항 4) p.add_argument("–openai-base-url", help="OpenAI base URL (기본: https://api.openai.com/v1)") p.add_argument("–openai-responses-path", help="OpenAI responses path (기본: /responses)") p.add_argument("–ollama-host", help="Ollama host (기본: http://localhost:11434)") sub = p.add_subparsers(dest="command", required=True) return p, sub

def main(): # 1) 파서/서브파서 생성 parser, sub = build_parser()

# 2) 커맨드 자동 로드(내장 + 외부 플러그인)
autoload_builtin_commands(REGISTRY)
autoload_plugin_entrypoints(REGISTRY)
REGISTRY.build_subparsers(sub)

# 3) 파싱 및 설정 로드(ENDPOINT 포함)
args = parser.parse_args()
cfg = load_config(args)

# 4) 의존 주입
deps = AppDeps(
    cfg=cfg,
    openai=openai_backend.call_openai,
    ollama=type("OllamaAPI", (), {
        "chat": ollama_backend.chat,
        "generate": ollama_backend.generate
    })(),
    presets=presets_mod
)

# 5) 실행
if not hasattr(args, "_handler"):
    parser.print_help(); return
args._handler(args, deps)

if name == "main": main()

## 파일: `pyproject.toml`

toml

[project] name = "simpleai" version = "0.1.0" description = "Simple, extensible AI CLI (OpenAI/Ollama)" authors = [{name="you"}] readme = "README.md" requires-python = ">=3.9" dependencies = ["requests"]

[project.scripts] simpleai = "simpleai.cli:main"

외부 플러그인도 가능: 다른 패키지에서 아래 그룹으로 entry point 등록

[project.entry-points."simpleai.commands"]

예) 외부패키지에서: rewrite = "myplugin.rewrite:register"

## 파일: `README.md` (요약)

md

simpleai

단일 CLI로 번역/요약/설명/커밋/TODO/Docstring/분류를 수행합니다.

설치

pip install -e .

scss

설정(ENDPOINT)

  • CLI가 최우선, 없으면 ENV, 없으면 YAML(옵션)

ENV 예시:

export OPENAI_API_KEY=sk-...  
export SIMPLEAI_BACKEND=openai  
export SIMPLEAI_OPENAI_BASE_URL=https://api.openai.com/v1  
export SIMPLEAI_OPENAI_RESPONSES_PATH=/responses  
export SIMPLEAI_OLLAMA_HOST=http://localhost:11434

yaml

YAML 예시: ~/.simpleai/config.yaml

defaults:
  backend: openai
openai:
  base_url: https://api.openai.com/v1
  responses_path: /responses
  model: gpt-4o-mini
ollama:
  host: http://localhost:11434
  chat_path: /api/chat
  generate_path: /api/generate
  model: llama3.2

사용 예시

vbnet

simpleai translate --to en --text "안녕?"
echo "회의 노트..." | simpleai summarize
simpleai commit --git
simpleai classify --text "DB 연결 오류 발생"

makefile

## 파일: `.env.example`
```env
OPENAI_API_KEY=sk-xxx
SIMPLEAI_BACKEND=openai
SIMPLEAI_OPENAI_BASE_URL=https://api.openai.com/v1
SIMPLEAI_OPENAI_RESPONSES_PATH=/responses
SIMPLEAI_OLLAMA_HOST=http://localhost:11434

파일: examples/example.py

python

def fib(n):
    if n <= 1: return n
    return fib(n-1)+fib(n-2)

파일: examples/notes.txt

csharp

We discussed migration steps, risks (downtime), and rollout plan with canary deployments...

파일: examples/policy.txt

sql

SLA: critical fixes within 24 hours. Minor issues within 7 days.

파일: tests/test_smoke.py

python

import subprocess

def test_cli_help():
    out = subprocess.check_output(["python","-m","simpleai.cli","translate","-h"], text=True)
    assert "번역" in out


3) “새로운 subcommand”를 쉽게 추가하는 구조

핵심은 registry.register(name, add_arguments, handler) 입니다.

  • commands/ 폴더에 *.py 추가 → 모듈 import 시 register(REGISTRY) 호출

  • 외부 패키지에서도 entry points (simpleai.commands)로 등록 가능

예시 A: 내부에 rewrite 추가 (파일 simpleai/commands/rewrite.py)

python

import argparse
from simpleai.registry import REGISTRY, AppDeps
from simpleai.utils import read_input_text

SYS_PROMPT = (
    "역할: 문장 다듬기 도우미\n"
    "- 한국어를 더 자연스럽고 간결하게 재작성\n"
    "- 의미 보존, 존칭 유지\n"
    "- 출력은 재작성된 문장만\n"
)

def _add(sub):
    sp = sub.add_parser("rewrite", help="문장 다듬기")
    sp.add_argument("--text")
    sp.add_argument("--file")
    return sp

def _handle(args: argparse.Namespace, deps: AppDeps):
    text = read_input_text(args)
    if deps.cfg.backend == "openai":
        out = deps.openai(
            instructions=SYS_PROMPT, user_text=text,
            base_url=deps.cfg.openai.base_url,
            responses_path=deps.cfg.openai.responses_path,
            api_key=deps.cfg.openai.api_key,
            model=deps.cfg.openai.model,
        )
    else:
        out = deps.ollama.chat(
            instructions=SYS_PROMPT, user_text=text,
            host=deps.cfg.ollama.host, chat_path=deps.cfg.ollama.chat_path,
            model=deps.cfg.ollama.model,
        )
    print(out)

def register(registry=REGISTRY):
    registry.register("rewrite", _add, _handle)

register()

설치 후: simpleai rewrite --text "회의 일정 정리 부탁드립니다"

예시 B: 외부 패키지 플러그인으로 추가

  1. 외부 패키지의 pyproject.toml: ``` toml

[project.entry-points."simpleai.commands"] rewrite = "myplugin.rewrite:register"

  2. `myplugin/rewrite.py`에 위와 동일한 `register(registry)` 구현

  3. 사용: `pip install myplugin` → `simpleai rewrite ...` 자동 인식

* * *

# 4) ENDPOINT 주소를 내가 등록하는 방법

**우선순위** : `CLI 옵션` > `환경변수` > `YAML 설정` > **기본값**

  * OpenAI

    * CLI: `--openai-base-url`, `--openai-responses-path`, `--model`

    * ENV: `SIMPLEAI_OPENAI_BASE_URL`, `SIMPLEAI_OPENAI_RESPONSES_PATH`, `OPENAI_MODEL`

    * YAML: `openai.base_url`, `openai.responses_path`, `openai.model`

  * Ollama

    * CLI: `--ollama-host`, `--model`

    * ENV: `SIMPLEAI_OLLAMA_HOST`, `OLLAMA_MODEL`, (경로 커스터마이즈) `SIMPLEAI_OLLAMA_CHAT_PATH`, `SIMPLEAI_OLLAMA_GENERATE_PATH`

    * YAML: `ollama.host`, `ollama.chat_path`, `ollama.generate_path`, `ollama.model`

**재현 가능한 설정 예시**

bash

OpenAI를 사설 프록시로

simpleai –backend openai –openai-base-url https://myproxy/v1 –openai-responses-path /responses translate –to en –text "안녕?"

Ollama를 원격 호스트로

simpleai –backend ollama –ollama-host http://10.0.0.5:11434 summarize –file examples/notes.txt

* * *

## 마무리 (요약)

  * **이름 10개** 제공,

  * **패키지 구조 + 전 파일** 제공(실행 가능),

  * **서브커맨드 확장** : 내부 파일 추가 or **entry points 플러그인** ,

  * **ENDPOINT 설정** : CLI·ENV·YAML로 손쉽게 오버라이드.

원하시면 패키지명을 당신 선호안으로 바꿔서 전체 파일을 다시 리렌더링해 드릴게요.

<p class="chat--user" markdown="1">

miniai로 할게. 사실 사내망 환경에서 사내 gpu서버의 endpoint를 무조건 사용할거야. 그래서 chatgpt 서버나 ollama 세팅은 필요없어. 일단 그냥 커스텀 openai compatible endpoint만 받으면 되고 각 세팅(endpoint 주소, 키, 모델명)은 환경 변수로부터 읽어오도록 다시 작성해줘.

</p>

좋아요. 요청하신 대로 **사내 GPU 서버의 OpenAI-호환(“chat/completions”) 엔드포인트만** 쓰는 버전으로, **환경 변수에서 엔드포인트/키/모델** 을 읽고, **확장 가능한 서브커맨드 구조** 를 유지한 `miniai` 프로젝트를 제공합니다.  
(간단한 단계 → 전체 코드 → 사용 예 → 서브커맨드 확장 예 순서예요.)

* * *

# 0) 한눈에(개념 단계)

  1. **의존성 최소화** : `requests`만 사용.

  2. **백엔드 단일화** : OpenAI-compatible **Chat Completions** 만 호출(`/v1/chat/completions`).

  3. **환경 변수만** : `MINIAI_API_BASE`, `MINIAI_API_PATH`, `MINIAI_API_KEY`, `MINIAI_MODEL`(필수), `MINIAI_TEMPERATURE`, `MINIAI_TIMEOUT`, `MINIAI_EXTRA_HEADERS`(옵션).

  4. **확장성** : `CommandRegistry` \+ `commands/` 자동 로딩 + (선택) entry points 플러그인.

  5. **UX** : `miniai translate "안녕?"` 처럼 **포지셔널 인자** 지원(`--text/--file/STDIN`도 가능).

* * *

# 1) 환경 변수 (필수/옵션)

bash

필수

export MINIAI_API_BASE="https://llm.company.local/v1" export MINIAI_API_KEY="sk-****" export MINIAI_MODEL="my-internal-chat"

선택(기본값 표시)

export MINIAI_API_PATH="/chat/completions" # 기본: /chat/completions export MINIAI_TEMPERATURE="0.2" # 기본: 0.2 export MINIAI_TIMEOUT="120" # 초, 기본: 120

추가 헤더가 필요하면 JSON으로

export MINIAI_EXTRA_HEADERS='{"X-Cluster":"a100","X-Project":"nlp"}'

* * *

# 2) 프로젝트 구조

markdown

miniai/ ├─ miniai/ │ ├─ init.py │ ├─ cli.py │ ├─ registry.py │ ├─ presets.py │ ├─ utils.py │ ├─ backends/ │ │ ├─ init.py │ │ └─ openai_compat.py │ └─ commands/ │ ├─ init.py │ ├─ translate.py │ ├─ summarize.py │ ├─ explain.py │ ├─ commit.py │ ├─ todo.py │ ├─ docstring.py │ ├─ classify.py │ └─ rewrite.py # 새 subcommand 예시 ├─ pyproject.toml ├─ README.md └─ .env.example

* * *

# 3) 전체 파일

## `miniai/__init__.py`

python

all = ["cli"] version = "0.1.0"

## `miniai/presets.py`

python

def preset_translate(to="en") -> str: return ( "역할: 고품질 번역기\n" f"- 목표 언어: {to}\n" "- 의미/뉘앙스 보존, 자연스러운 의역\n" "- 출력은 번역문만(주석 금지)\n" )

PRESETS = { "summarize": ( "역할: 전문 요약가\n" "- 한국어 출력, 불릿 최대 7개, 명사형 간결체\n" "- 핵심 주장/근거/위험/다음 액션 우선\n" ), "explain": ( "역할: 코드 리뷰어\n" "- 한국어 3섹션: [설명] [복잡도] [리팩터링]\n" "- 핵심 로직, 시간/공간 복잡도, 개선안\n" ), "commit": ( "역할: Conventional Commits 작성기\n" "- 한국어, 제목 72자 이내 + 1~2줄 본문\n" "- feat/fix/docs/refactor/test/chore 중 선택\n" "- 출력은 메시지 본문만\n" ), "todo": ( "역할: TODO 추출기\n" "- 입력에서 실행 가능한 액션만 '- [ ] ' 체크리스트로 출력\n" ), "docstring": ( "역할: 파이썬 Docstring 작성기\n" "- 한국어 PEP257 스타일\n" "- unified diff 패치 형식(+++ 파일명 포함)\n" ), "classify": ( "역할: 간단 이슈 분류기\n" "- 입력 한 줄 → JSON {category, priority}\n" '- category ∈ {"bug","docs","feature"}, priority ∈ {"high","low"}\n' "- 출력은 순수 JSON 한 객체만\n" ), }

## `miniai/registry.py`

python

import argparse, importlib, pkgutil from typing import Callable, Dict, List from importlib import metadata

Handler = Callable[[argparse.Namespace, "AppDeps"], None] Adder = Callable[[argparse._SubParsersAction], argparse.ArgumentParser]

class CommandRegistry: def init(self): self._commands: Dict[str, Dict[str, object]] = {}

def register(self, name: str, add_arguments: Adder, handler: Handler):
    if name in self._commands:
        raise ValueError(f"duplicate command: {name}")
    self._commands[name] = {"add_arguments": add_arguments, "handler": handler}

def build_subparsers(self, sub: argparse._SubParsersAction):
    for name, spec in self._commands.items():
        sp = spec["add_arguments"](sub)
        sp.set_defaults(_handler=spec["handler"])

def names(self) -> List[str]:
    return sorted(self._commands.keys())

def autoload_builtin_commands(registry: CommandRegistry): import miniai.commands as pkg for mod in pkgutil.iter_modules(pkg.path, pkg.name + "."): importlib.import_module(mod.name)

def autoload_plugin_entrypoints(registry: CommandRegistry): # 선택: 외부 패키지가 entry points로 등록한 커맨드 자동 로드 try: eps = metadata.entry_points() group = eps.select(group="miniai.commands") except Exception: group = [] for ep in group: register_fn = ep.load() register_fn(registry)

class AppDeps: def init(self, chat_call, presets): self.chat_call = chat_call self.presets = presets

REGISTRY = CommandRegistry()

## `miniai/utils.py`

python

import os, sys, json, subprocess

def read_input_text(args) -> str: # 1) 포지셔널 인자 2) –text 3) –file 4) STDIN if getattr(args, "pos_text", None): return args.pos_text if getattr(args, "text", None): return args.text if getattr(args, "file", None): with open(args.file, "r", encoding="utf-8") as f: return f.read() if not sys.stdin.isatty(): return sys.stdin.read() raise SystemExit("입력이 없습니다. "miniai '텍스트'\" 또는 --text/--file/STDIN을 사용하세요.")

def git_diff(staged: bool=False) -> str: cmd = ["git", "diff", "–staged"] if staged else ["git", "diff"] out = subprocess.check_output(cmd, encoding="utf-8", stderr=subprocess.STDOUT) if not out.strip(): raise SystemExit("git diff 결과가 비어 있습니다.") return out

def env_or_die(name: str) -> str: v = os.getenv(name) if not v: raise SystemExit(f"환경변수 {name} 가 필요합니다.") return v

def load_env_config(): base = env_or_die("MINIAI_API_BASE").rstrip("/") path = os.getenv("MINIAI_API_PATH", "/chat/completions") key = env_or_die("MINIAI_API_KEY") model= env_or_die("MINIAI_MODEL") temp = float(os.getenv("MINIAI_TEMPERATURE", "0.2")) timeout = int(os.getenv("MINIAI_TIMEOUT", "120")) extra_headers = os.getenv("MINIAI_EXTRA_HEADERS") headers = {} if extra_headers: try: headers = json.loads(extra_headers) except Exception: raise SystemExit("MINIAI_EXTRA_HEADERS 값은 JSON 이어야 합니다.") return { "url": base + path, "api_key": key, "model": model, "temperature": temp, "timeout": timeout, "extra_headers": headers, }

## `miniai/backends/__init__.py`

python

placeholder

## `miniai/backends/openai_compat.py`

python

import json, requests

def chat_completions(url: str, api_key: str, model: str, system_text: str, user_text: str, *, temperature: float = 0.2, timeout: int = 120, extra_headers: dict | None = None, json_object: bool = False) -> str: headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} if extra_headers: headers.update(extra_headers)

payload = {
    "model": model,
    "messages": [
        {"role": "system", "content": system_text},
        {"role": "user", "content": user_text},
    ],
    "temperature": temperature,
}
# 일부 서버는 json_object 지원. (미지원이면 그냥 무시되거나 400)
if json_object:
    payload["response_format"] = {"type": "json_object"}

r = requests.post(url, headers=headers, data=json.dumps(payload), timeout=timeout)
if r.status_code != 200:
    raise SystemExit(f"[miniai] HTTP {r.status_code}: {r.text}")

data = r.json()
# 표준: choices[0].message.content
try:
    return data["choices"][0]["message"]["content"]
except Exception:
    # 비표준 대응
    return data.get("content") or data.get("text") or json.dumps(data, ensure_ascii=False)
## `miniai/commands/__init__.py`

python

각 모듈이 import될 때 registry에 자신을 등록합니다.

### 공통 패턴(각 커맨드가 이 패턴을 따릅니다)

  * 포지셔널 인자 `pos_text` \+ `--text/--file`

  * `deps.chat_call(...)`로 호출

## `miniai/commands/translate.py`

python

import argparse from miniai.registry import REGISTRY, AppDeps from miniai.utils import read_input_text from miniai.presets import preset_translate

def _add(sub): sp = sub.add_parser("translate", help="번역") sp.add_argument("pos_text", nargs="?", help="입력 텍스트(포지셔널)") sp.add_argument("–text") sp.add_argument("–file") sp.add_argument("–to", default="en", help="목표 언어 코드(기본=en)") return sp

def _handle(args: argparse.Namespace, deps: AppDeps): sys_prompt = preset_translate(to=args.to) text = read_input_text(args) out = deps.chat_call(system_text=sys_prompt, user_text=text, json_object=False) print(out)

def register(registry=REGISTRY): registry.register("translate", _add, _handle)

register()

## `miniai/commands/summarize.py`

python

import argparse from miniai.registry import REGISTRY, AppDeps from miniai.utils import read_input_text from miniai.presets import PRESETS

def _add(sub): sp = sub.add_parser("summarize", help="요약(불릿 7)") sp.add_argument("pos_text", nargs="?") sp.add_argument("–text"); sp.add_argument("–file") return sp

def _handle(args: argparse.Namespace, deps: AppDeps): sys_prompt = PRESETS["summarize"] text = read_input_text(args) out = deps.chat_call(system_text=sys_prompt, user_text=text, json_object=False) print(out)

def register(registry=REGISTRY): registry.register("summarize", _add, _handle)

register()

## `miniai/commands/explain.py`

python

import argparse from miniai.registry import REGISTRY, AppDeps from miniai.utils import read_input_text from miniai.presets import PRESETS

def _add(sub): sp = sub.add_parser("explain", help="코드 설명+개선") sp.add_argument("pos_text", nargs="?") sp.add_argument("–text"); sp.add_argument("–file") return sp

def _handle(args: argparse.Namespace, deps: AppDeps): sys_prompt = PRESETS["explain"] text = read_input_text(args) out = deps.chat_call(system_text=sys_prompt, user_text=text, json_object=False) print(out)

def register(registry=REGISTRY): registry.register("explain", _add, _handle)

register()

## `miniai/commands/commit.py`

python

import argparse from miniai.registry import REGISTRY, AppDeps from miniai.utils import read_input_text, git_diff from miniai.presets import PRESETS

def _add(sub): sp = sub.add_parser("commit", help="Conventional Commits 생성") sp.add_argument("pos_text", nargs="?") sp.add_argument("–text"); sp.add_argument("–file") sp.add_argument("–git", action="store_true") sp.add_argument("–staged", action="store_true") return sp

def _handle(args: argparse.Namespace, deps: AppDeps): sys_prompt = PRESETS["commit"] if args.git or args.staged: text = git_diff(staged=args.staged) else: text = read_input_text(args) user_msg = f"[변경점]\n{text}" out = deps.chat_call(system_text=sys_prompt, user_text=user_msg, json_object=False) print(out)

def register(registry=REGISTRY): registry.register("commit", _add, _handle)

register()

## `miniai/commands/todo.py`

python

import argparse from miniai.registry import REGISTRY, AppDeps from miniai.utils import read_input_text from miniai.presets import PRESETS

def _add(sub): sp = sub.add_parser("todo", help="문서에서 TODO 추출") sp.add_argument("pos_text", nargs="?") sp.add_argument("–text"); sp.add_argument("–file") return sp

def _handle(args: argparse.Namespace, deps: AppDeps): sys_prompt = PRESETS["todo"] text = read_input_text(args) out = deps.chat_call(system_text=sys_prompt, user_text=text, json_object=False) print(out)

def register(registry=REGISTRY): registry.register("todo", _add, _handle)

register()

## `miniai/commands/docstring.py`

python

import argparse from miniai.registry import REGISTRY, AppDeps from miniai.utils import read_input_text from miniai.presets import PRESETS

def _add(sub): sp = sub.add_parser("docstring", help="파이썬 파일 Docstring 패치 제안") sp.add_argument("pos_text", nargs="?") sp.add_argument("–text"); sp.add_argument("–file") return sp

def _handle(args: argparse.Namespace, deps: AppDeps): sys_prompt = PRESETS["docstring"] text = read_input_text(args) user_msg = f"[파일]\n{text}" out = deps.chat_call(system_text=sys_prompt, user_text=user_msg, json_object=False) print(out)

def register(registry=REGISTRY): registry.register("docstring", _add, _handle)

register()

## `miniai/commands/classify.py`

python

import argparse, json, re from miniai.registry import REGISTRY, AppDeps from miniai.utils import read_input_text from miniai.presets import PRESETS

def _add(sub): sp = sub.add_parser("classify", help="문장을 JSON으로 라벨링(category/priority)") sp.add_argument("pos_text", nargs="?") sp.add_argument("–text"); sp.add_argument("–file") return sp

def _handle(args: argparse.Namespace, deps: AppDeps): sys_prompt = PRESETS["classify"] text = read_input_text(args).strip() # json_object 모드 시도(미지원 서버도 있어 폴백 처리) raw = deps.chat_call(system_text=sys_prompt, user_text=text, json_object=True) try: print(json.dumps(json.loads(raw), ensure_ascii=False)) return except Exception: # 응답 안에 JSON이 섞여 있으면 추출 m = re.search(r"{.*}", raw, re.S) if m: try: print(json.dumps(json.loads(m.group(0)), ensure_ascii=False)) return except Exception: pass print(raw) # 최후 폴백 return

def register(registry=REGISTRY): registry.register("classify", _add, _handle)

register()

## `miniai/commands/rewrite.py` (새 subcommand 예시)

python

import argparse from miniai.registry import REGISTRY, AppDeps from miniai.utils import read_input_text

SYS_PROMPT = ( "역할: 문장 다듬기 도우미\n" "- 한국어를 더 자연스럽고 간결하게 재작성\n" "- 의미 보존, 존칭 유지\n" "- 출력은 재작성된 문장만\n" )

def _add(sub): sp = sub.add_parser("rewrite", help="문장 다듬기") sp.add_argument("pos_text", nargs="?") sp.add_argument("–text"); sp.add_argument("–file") return sp

def _handle(args: argparse.Namespace, deps: AppDeps): text = read_input_text(args) out = deps.chat_call(system_text=SYS_PROMPT, user_text=text, json_object=False) print(out)

def register(registry=REGISTRY): registry.register("rewrite", _add, _handle)

register()

## `miniai/cli.py`

python

#!/usr/bin/env python3 import argparse from miniai.registry import REGISTRY, autoload_builtin_commands, autoload_plugin_entrypoints, AppDeps from miniai.backends.openai_compat import chat_completions from miniai.utils import load_env_config from miniai import presets as presets_mod

def build_parser(): p = argparse.ArgumentParser(prog="miniai", description="Minimal AI CLI (OpenAI-compatible only)") sub = p.add_subparsers(dest="command", required=True) return p, sub

def main(): parser, sub = build_parser() autoload_builtin_commands(REGISTRY) autoload_plugin_entrypoints(REGISTRY) REGISTRY.build_subparsers(sub)

args = parser.parse_args()
env = load_env_config()

def chat_call(system_text: str, user_text: str, json_object: bool=False) -> str:
    return chat_completions(
        url=env["url"],
        api_key=env["api_key"],
        model=env["model"],
        system_text=system_text,
        user_text=user_text,
        temperature=env["temperature"],
        timeout=env["timeout"],
        extra_headers=env["extra_headers"],
        json_object=json_object,
    )

deps = AppDeps(chat_call=chat_call, presets=presets_mod)

if not hasattr(args, "_handler"):
    parser.print_help(); return
args._handler(args, deps)

if name == "main": main()

## `pyproject.toml`

toml

[project] name = "miniai" version = "0.1.0" description = "Minimal, extensible AI CLI for OpenAI-compatible endpoints" authors = [{name="you"}] readme = "README.md" requires-python = ">=3.9" dependencies = ["requests"]

[project.scripts] miniai = "miniai.cli:main"

선택: 외부 패키지가 새 커맨드를 플러그인으로 배포할 때

[project.entry-points."miniai.commands"]

예) 외부 패키지에서: rewrite = "myplugin.rewrite:register"

## `.env.example`

env

필수

MINIAI_API_BASE=https://llm.company.local/v1 MINIAI_API_KEY=sk-xxxxxxxx MINIAI_MODEL=my-internal-chat

선택

MINIAI_API_PATH=/chat/completions MINIAI_TEMPERATURE=0.2 MINIAI_TIMEOUT=120

JSON 문자열

MINIAI_EXTRA_HEADERS={"X-Cluster":"a100","X-Project":"nlp"}

## `README.md` (요약)

md

miniai

사내 OpenAI-호환 엔드포인트만 사용하는 경량 AI CLI.

설치

pip install -e .

환경 변수

MINIAI_API_BASE, MINIAI_API_KEY, MINIAI_MODEL (필수)
기타: MINIAI_API_PATH=/chat/completions, MINIAI_TEMPERATURE=0.2, MINIAI_TIMEOUT=120, MINIAI_EXTRA_HEADERS='{"K":"V"}'

사용 예

miniai translate "안녕?" –to en echo "회의 노트…" | miniai summarize miniai commit –git miniai classify "DB 연결 오류 발생"

* * *

# 4) 새 서브커맨드 추가 방법(확장성)

  * **내부 추가** : `miniai/commands/`에 `foo.py` 생성 → `register(REGISTRY)` 호출 → 자동 인식.

  * **외부 플러그인** : 다른 패키지에서 `pyproject.toml`의 `[project.entry-points."miniai.commands"]`에 등록 → 설치 시 자동 로드.

### 예: `foo` 커맨드(간단 생성기)

python

파일: miniai/commands/foo.py

import argparse from miniai.registry import REGISTRY, AppDeps

SYS = "역할: 짧은 한 줄 생성기\n- 한국어 한 줄만 출력"

def _add(sub): sp = sub.add_parser("foo", help="한 줄 생성") sp.add_argument("pos_text", nargs="?", help="프롬프트") sp.add_argument("–text"); sp.add_argument("–file") return sp

def _handle(args: argparse.Namespace, deps: AppDeps): prompt = args.pos_text or args.text or "예시 문장" print(deps.chat_call(system_text=SYS, user_text=prompt, json_object=False))

def register(registry=REGISTRY): registry.register("foo", _add, _handle)

register()

* * *

# 5) 빠른 테스트

bash

pip install -e .

환경 변수 설정(위 .env.example 참고)

miniai translate "안녕?" –to en miniai summarize "마이그레이션 일정과 위험…" cat my.py | miniai explain miniai commit –git

Categories:

Updated: