ltcai 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ltcai-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TaeSoo Park
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ltcai-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,235 @@
1
+ Metadata-Version: 2.4
2
+ Name: ltcai
3
+ Version: 0.1.0
4
+ Summary: Lattice AI local MLX/cloud LLM workspace server
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/TaeSooPark-PTS/LatticeAI
7
+ Project-URL: Repository, https://github.com/TaeSooPark-PTS/LatticeAI
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: fastapi
12
+ Requires-Dist: uvicorn
13
+ Requires-Dist: pydantic
14
+ Requires-Dist: httpx
15
+ Requires-Dist: pillow
16
+ Requires-Dist: openai
17
+ Requires-Dist: python-docx
18
+ Requires-Dist: openpyxl
19
+ Requires-Dist: python-pptx
20
+ Requires-Dist: python-multipart
21
+ Requires-Dist: keyring
22
+ Provides-Extra: local
23
+ Requires-Dist: mlx-lm; extra == "local"
24
+ Requires-Dist: mlx-vlm; extra == "local"
25
+ Provides-Extra: voice
26
+ Requires-Dist: openai-whisper; extra == "voice"
27
+ Requires-Dist: SpeechRecognition; extra == "voice"
28
+ Requires-Dist: pydub; extra == "voice"
29
+ Provides-Extra: all
30
+ Requires-Dist: mlx-lm; extra == "all"
31
+ Requires-Dist: mlx-vlm; extra == "all"
32
+ Requires-Dist: openai-whisper; extra == "all"
33
+ Requires-Dist: SpeechRecognition; extra == "all"
34
+ Requires-Dist: pydub; extra == "all"
35
+ Dynamic: license-file
36
+
37
+ # Lattice AI
38
+
39
+ Local/cloud LLM workspace server with MLX, Ollama, vLLM, OpenAI-compatible providers,
40
+ BYOK API keys, MCP recommendations, and editor extensions for VS Code, Cursor, and Antigravity.
41
+
42
+ ---
43
+
44
+ ## 아키텍처
45
+
46
+ ```
47
+ Lattice AI/
48
+ ├── server.py # FastAPI bridge server (port 4825)
49
+ ├── llm_router.py # local/cloud model router
50
+ ├── tools.py # local workspace tools
51
+ ├── static/ # web UI
52
+ ├── bin/ltcai.js # npm CLI entrypoint
53
+ ├── pyproject.toml # PyPI metadata
54
+ └── vscode-extension/ # VS Code/Cursor/Antigravity extension
55
+ ```
56
+
57
+ ---
58
+
59
+ ## 빠른 시작
60
+
61
+ ### 1. 서버 설치 & 실행
62
+
63
+ ```bash
64
+ # PyPI
65
+ pip install ltcai
66
+
67
+ # 로컬 MLX까지 함께 쓰려면
68
+ pip install "ltcai[local]"
69
+
70
+ # npm
71
+ npm install -g ltcai
72
+
73
+ # 서버 실행
74
+ LTCAI
75
+ # → http://localhost:4825 에서 실행됨
76
+ ```
77
+
78
+ 개발 중에는 설치 없이도 실행할 수 있습니다.
79
+
80
+ ```bash
81
+ python ltcai_cli.py
82
+ python ltcai_cli.py --reload
83
+ LTCAI doctor
84
+ ```
85
+
86
+ `npm install -g ltcai`로 설치한 경우 첫 실행 시 `~/.ltcai/npm-python`에 Python 가상환경을 만들고
87
+ `requirements.txt`를 설치합니다. 자동 설치를 끄려면 `LTCAI_SKIP_NPM_BOOTSTRAP=1`을 설정하세요.
88
+
89
+ Lattice AI stores runtime data in `~/.ltcai/` by default. Override it with
90
+ `LATTICEAI_DATA_DIR=/path/to/data` when running `LTCAI`.
91
+
92
+ ### 2. 첫 모델 로드 (터미널 or 확장 프로그램에서)
93
+
94
+ ```bash
95
+ # 터미널에서 직접
96
+ curl -X POST http://localhost:4825/models/load \
97
+ -H "Content-Type: application/json" \
98
+ -d '{"model_id": "mlx-community/Qwen2.5-Coder-7B-Instruct-4bit"}'
99
+ ```
100
+
101
+ 또는 확장 프로그램에서 `Cmd+Shift+M` → 모델 선택
102
+
103
+ ### 3. 확장 프로그램 설치
104
+
105
+ ```bash
106
+ cd vscode-extension
107
+ npm install
108
+ npm run build
109
+ npm run package:vsix
110
+
111
+ # VS Code / Cursor / Antigravity에서:
112
+ # 1. Extensions 패널 → "..." → "Install from VSIX" 또는
113
+ # 2. 로컬 CLI가 있으면:
114
+ npm run install:all
115
+ ```
116
+
117
+ ---
118
+
119
+ ## 모델/비용 구조
120
+
121
+ - Local LLM: MLX, Ollama, vLLM, LM Studio, llama.cpp
122
+ - Cloud LLM: OpenAI, OpenRouter, Groq, Together, xAI 등 OpenAI-compatible provider
123
+ - API 비용: 사용자가 본인 API key를 입력하는 BYOK 구조입니다. 사용자별 키로 호출되므로 키 소유자가 사용량을 부담합니다.
124
+ - 초대 링크 게이트는 기본 비활성화되어 있습니다. 다시 켜려면 `LATTICEAI_INVITE_GATE_ENABLED=true`를 설정하세요.
125
+
126
+ ## 보안 기본값
127
+
128
+ - 기본 서버 바인딩은 `127.0.0.1:4825`입니다. 같은 네트워크에서 접속하게 하려면 명시적으로 `LATTICEAI_HOST=0.0.0.0`을 설정하세요.
129
+ - CORS는 기본적으로 localhost만 허용합니다. 네트워크 공개가 필요하면 `LATTICEAI_CORS_ALLOW_NETWORK=true`를 명시적으로 설정하세요.
130
+ - 사용자 API key는 OS keyring/Keychain에 저장합니다. keyring을 사용할 수 없는 환경에서 평문 저장을 허용하려면 `LATTICEAI_ALLOW_PLAINTEXT_API_KEYS=true`를 직접 설정해야 합니다.
131
+ - 히스토리 저장 전 API key/token/password 패턴은 마스킹됩니다.
132
+
133
+ ## 지원 모델 예시 (M5 32GB 기준)
134
+
135
+ | 모델 | 용도 | 크기 | 추천도 |
136
+ |------|------|------|--------|
137
+ | `mlx-community/Qwen2.5-Coder-7B-Instruct-4bit` | 코딩 | ~4GB | ⭐⭐⭐ |
138
+ | `mlx-community/Qwen2.5-Coder-14B-Instruct-4bit` | 코딩 | ~8GB | ⭐⭐⭐⭐ |
139
+ | `mlx-community/Qwen2.5-Coder-32B-Instruct-4bit` | 코딩 | ~18GB | ⭐⭐⭐⭐⭐ |
140
+ | `mlx-community/Llama-3.1-8B-Instruct-4bit` | 범용 | ~4.5GB| ⭐⭐⭐ |
141
+ | `mlx-community/DeepSeek-R1-0528-4bit` | 추론 | ~38GB | ⭐⭐⭐⭐ |
142
+ | `mlx-community/Phi-4-4bit` | 코딩 | ~8GB | ⭐⭐⭐⭐ |
143
+ | `mlx-community/gemma-3-27b-it-4bit` | 범용 | ~15GB | ⭐⭐⭐ |
144
+
145
+ > **M5 32GB 추천**: Qwen2.5-Coder-32B-Instruct-4bit (18GB) — 32GB에서 여유롭게 동작
146
+
147
+ ---
148
+
149
+ ## 멀티모델 핫스왑
150
+
151
+ 여러 모델을 동시에 메모리에 올려두고 즉시 전환 가능:
152
+
153
+ ```bash
154
+ # 모델 A 로드
155
+ curl -X POST localhost:4825/models/load -d '{"model_id":"mlx-community/Qwen2.5-Coder-7B-Instruct-4bit"}'
156
+
157
+ # 모델 B도 함께 로드
158
+ curl -X POST localhost:4825/models/load -d '{"model_id":"mlx-community/Llama-3.1-8B-Instruct-4bit"}'
159
+
160
+ # B → A 즉시 전환 (재로드 없음)
161
+ curl -X POST localhost:4825/models/switch/mlx-community%2FQwen2.5-Coder-7B-Instruct-4bit
162
+
163
+ # 메모리 해제
164
+ curl -X DELETE localhost:4825/models/unload/mlx-community%2FLlama-3.1-8B-Instruct-4bit
165
+ ```
166
+
167
+ ---
168
+
169
+ ## 키보드 단축키
170
+
171
+ | 단축키 | 기능 |
172
+ |--------|------|
173
+ | `Cmd+Shift+A` | 채팅 패널 열기 |
174
+ | `Cmd+Shift+E` | 선택 코드 편집 (선택 필요) |
175
+ | `Cmd+Shift+M` | 모델 로드 / 전환 |
176
+ | 우클릭 메뉴 | Explain / Edit / Garden에 저장 |
177
+
178
+ ---
179
+
180
+ ## P-Reinforce 지식 정원사
181
+
182
+ 지식은 `~/.ltcai-ai-brain/`에 자동 분류 저장:
183
+
184
+ ```
185
+ ~/.ltcai-ai-brain/
186
+ ├── INDEX.md
187
+ ├── 00_Raw/ # 원시 데이터, 아이디어
188
+ ├── 10_Wiki/ # 검증된 개념, 레퍼런스
189
+ ├── 20_Skills/ # 코드 스니펫, 프롬프트
190
+ ├── 30_Projects/ # 프로젝트 컨텍스트
191
+ └── 40_Log/ # 날짜별 작업 로그
192
+ ```
193
+
194
+ 사용법: 에디터에서 텍스트 선택 → 우클릭 → **"Save to Knowledge Garden"**
195
+
196
+ ---
197
+
198
+ ## API 엔드포인트
199
+
200
+ | Method | Path | 설명 |
201
+ |--------|------|------|
202
+ | GET | `/health` | 서버 상태, 현재 모델 |
203
+ | GET | `/models` | 추천 모델 목록 + 로드 상태 |
204
+ | POST | `/models/load` | 모델 로드 (캐시 지원) |
205
+ | POST | `/models/switch/{id}` | 활성 모델 전환 |
206
+ | DELETE | `/models/unload/{id}` | 모델 언로드 |
207
+ | POST | `/chat` | 생성 (stream=true/false) |
208
+ | POST | `/garden` | P-Reinforce 저장 |
209
+ | GET | `/garden/tree` | 지식 트리 조회 |
210
+
211
+ ---
212
+
213
+ ## 자동 시작 설정 (선택)
214
+
215
+ ```bash
216
+ # launchd plist로 Mac 부팅시 자동 시작
217
+ cat > ~/Library/LaunchAgents/com.ltcai.mlx.plist << 'EOF'
218
+ <?xml version="1.0" encoding="UTF-8"?>
219
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
220
+ <plist version="1.0">
221
+ <dict>
222
+ <key>Label</key><string>com.ltcai.mlx</string>
223
+ <key>ProgramArguments</key>
224
+ <array>
225
+ <string>/usr/bin/python3</string>
226
+ <string>/path/to/LTCAI-ai-mlx/server/server.py</string>
227
+ </array>
228
+ <key>RunAtLoad</key><true/>
229
+ <key>KeepAlive</key><true/>
230
+ </dict>
231
+ </plist>
232
+ EOF
233
+
234
+ launchctl load ~/Library/LaunchAgents/com.ltcai.mlx.plist
235
+ ```
ltcai-0.1.0/README.md ADDED
@@ -0,0 +1,199 @@
1
+ # Lattice AI
2
+
3
+ Local/cloud LLM workspace server with MLX, Ollama, vLLM, OpenAI-compatible providers,
4
+ BYOK API keys, MCP recommendations, and editor extensions for VS Code, Cursor, and Antigravity.
5
+
6
+ ---
7
+
8
+ ## 아키텍처
9
+
10
+ ```
11
+ Lattice AI/
12
+ ├── server.py # FastAPI bridge server (port 4825)
13
+ ├── llm_router.py # local/cloud model router
14
+ ├── tools.py # local workspace tools
15
+ ├── static/ # web UI
16
+ ├── bin/ltcai.js # npm CLI entrypoint
17
+ ├── pyproject.toml # PyPI metadata
18
+ └── vscode-extension/ # VS Code/Cursor/Antigravity extension
19
+ ```
20
+
21
+ ---
22
+
23
+ ## 빠른 시작
24
+
25
+ ### 1. 서버 설치 & 실행
26
+
27
+ ```bash
28
+ # PyPI
29
+ pip install ltcai
30
+
31
+ # 로컬 MLX까지 함께 쓰려면
32
+ pip install "ltcai[local]"
33
+
34
+ # npm
35
+ npm install -g ltcai
36
+
37
+ # 서버 실행
38
+ LTCAI
39
+ # → http://localhost:4825 에서 실행됨
40
+ ```
41
+
42
+ 개발 중에는 설치 없이도 실행할 수 있습니다.
43
+
44
+ ```bash
45
+ python ltcai_cli.py
46
+ python ltcai_cli.py --reload
47
+ LTCAI doctor
48
+ ```
49
+
50
+ `npm install -g ltcai`로 설치한 경우 첫 실행 시 `~/.ltcai/npm-python`에 Python 가상환경을 만들고
51
+ `requirements.txt`를 설치합니다. 자동 설치를 끄려면 `LTCAI_SKIP_NPM_BOOTSTRAP=1`을 설정하세요.
52
+
53
+ Lattice AI stores runtime data in `~/.ltcai/` by default. Override it with
54
+ `LATTICEAI_DATA_DIR=/path/to/data` when running `LTCAI`.
55
+
56
+ ### 2. 첫 모델 로드 (터미널 or 확장 프로그램에서)
57
+
58
+ ```bash
59
+ # 터미널에서 직접
60
+ curl -X POST http://localhost:4825/models/load \
61
+ -H "Content-Type: application/json" \
62
+ -d '{"model_id": "mlx-community/Qwen2.5-Coder-7B-Instruct-4bit"}'
63
+ ```
64
+
65
+ 또는 확장 프로그램에서 `Cmd+Shift+M` → 모델 선택
66
+
67
+ ### 3. 확장 프로그램 설치
68
+
69
+ ```bash
70
+ cd vscode-extension
71
+ npm install
72
+ npm run build
73
+ npm run package:vsix
74
+
75
+ # VS Code / Cursor / Antigravity에서:
76
+ # 1. Extensions 패널 → "..." → "Install from VSIX" 또는
77
+ # 2. 로컬 CLI가 있으면:
78
+ npm run install:all
79
+ ```
80
+
81
+ ---
82
+
83
+ ## 모델/비용 구조
84
+
85
+ - Local LLM: MLX, Ollama, vLLM, LM Studio, llama.cpp
86
+ - Cloud LLM: OpenAI, OpenRouter, Groq, Together, xAI 등 OpenAI-compatible provider
87
+ - API 비용: 사용자가 본인 API key를 입력하는 BYOK 구조입니다. 사용자별 키로 호출되므로 키 소유자가 사용량을 부담합니다.
88
+ - 초대 링크 게이트는 기본 비활성화되어 있습니다. 다시 켜려면 `LATTICEAI_INVITE_GATE_ENABLED=true`를 설정하세요.
89
+
90
+ ## 보안 기본값
91
+
92
+ - 기본 서버 바인딩은 `127.0.0.1:4825`입니다. 같은 네트워크에서 접속하게 하려면 명시적으로 `LATTICEAI_HOST=0.0.0.0`을 설정하세요.
93
+ - CORS는 기본적으로 localhost만 허용합니다. 네트워크 공개가 필요하면 `LATTICEAI_CORS_ALLOW_NETWORK=true`를 명시적으로 설정하세요.
94
+ - 사용자 API key는 OS keyring/Keychain에 저장합니다. keyring을 사용할 수 없는 환경에서 평문 저장을 허용하려면 `LATTICEAI_ALLOW_PLAINTEXT_API_KEYS=true`를 직접 설정해야 합니다.
95
+ - 히스토리 저장 전 API key/token/password 패턴은 마스킹됩니다.
96
+
97
+ ## 지원 모델 예시 (M5 32GB 기준)
98
+
99
+ | 모델 | 용도 | 크기 | 추천도 |
100
+ |------|------|------|--------|
101
+ | `mlx-community/Qwen2.5-Coder-7B-Instruct-4bit` | 코딩 | ~4GB | ⭐⭐⭐ |
102
+ | `mlx-community/Qwen2.5-Coder-14B-Instruct-4bit` | 코딩 | ~8GB | ⭐⭐⭐⭐ |
103
+ | `mlx-community/Qwen2.5-Coder-32B-Instruct-4bit` | 코딩 | ~18GB | ⭐⭐⭐⭐⭐ |
104
+ | `mlx-community/Llama-3.1-8B-Instruct-4bit` | 범용 | ~4.5GB| ⭐⭐⭐ |
105
+ | `mlx-community/DeepSeek-R1-0528-4bit` | 추론 | ~38GB | ⭐⭐⭐⭐ |
106
+ | `mlx-community/Phi-4-4bit` | 코딩 | ~8GB | ⭐⭐⭐⭐ |
107
+ | `mlx-community/gemma-3-27b-it-4bit` | 범용 | ~15GB | ⭐⭐⭐ |
108
+
109
+ > **M5 32GB 추천**: Qwen2.5-Coder-32B-Instruct-4bit (18GB) — 32GB에서 여유롭게 동작
110
+
111
+ ---
112
+
113
+ ## 멀티모델 핫스왑
114
+
115
+ 여러 모델을 동시에 메모리에 올려두고 즉시 전환 가능:
116
+
117
+ ```bash
118
+ # 모델 A 로드
119
+ curl -X POST localhost:4825/models/load -d '{"model_id":"mlx-community/Qwen2.5-Coder-7B-Instruct-4bit"}'
120
+
121
+ # 모델 B도 함께 로드
122
+ curl -X POST localhost:4825/models/load -d '{"model_id":"mlx-community/Llama-3.1-8B-Instruct-4bit"}'
123
+
124
+ # B → A 즉시 전환 (재로드 없음)
125
+ curl -X POST localhost:4825/models/switch/mlx-community%2FQwen2.5-Coder-7B-Instruct-4bit
126
+
127
+ # 메모리 해제
128
+ curl -X DELETE localhost:4825/models/unload/mlx-community%2FLlama-3.1-8B-Instruct-4bit
129
+ ```
130
+
131
+ ---
132
+
133
+ ## 키보드 단축키
134
+
135
+ | 단축키 | 기능 |
136
+ |--------|------|
137
+ | `Cmd+Shift+A` | 채팅 패널 열기 |
138
+ | `Cmd+Shift+E` | 선택 코드 편집 (선택 필요) |
139
+ | `Cmd+Shift+M` | 모델 로드 / 전환 |
140
+ | 우클릭 메뉴 | Explain / Edit / Garden에 저장 |
141
+
142
+ ---
143
+
144
+ ## P-Reinforce 지식 정원사
145
+
146
+ 지식은 `~/.ltcai-ai-brain/`에 자동 분류 저장:
147
+
148
+ ```
149
+ ~/.ltcai-ai-brain/
150
+ ├── INDEX.md
151
+ ├── 00_Raw/ # 원시 데이터, 아이디어
152
+ ├── 10_Wiki/ # 검증된 개념, 레퍼런스
153
+ ├── 20_Skills/ # 코드 스니펫, 프롬프트
154
+ ├── 30_Projects/ # 프로젝트 컨텍스트
155
+ └── 40_Log/ # 날짜별 작업 로그
156
+ ```
157
+
158
+ 사용법: 에디터에서 텍스트 선택 → 우클릭 → **"Save to Knowledge Garden"**
159
+
160
+ ---
161
+
162
+ ## API 엔드포인트
163
+
164
+ | Method | Path | 설명 |
165
+ |--------|------|------|
166
+ | GET | `/health` | 서버 상태, 현재 모델 |
167
+ | GET | `/models` | 추천 모델 목록 + 로드 상태 |
168
+ | POST | `/models/load` | 모델 로드 (캐시 지원) |
169
+ | POST | `/models/switch/{id}` | 활성 모델 전환 |
170
+ | DELETE | `/models/unload/{id}` | 모델 언로드 |
171
+ | POST | `/chat` | 생성 (stream=true/false) |
172
+ | POST | `/garden` | P-Reinforce 저장 |
173
+ | GET | `/garden/tree` | 지식 트리 조회 |
174
+
175
+ ---
176
+
177
+ ## 자동 시작 설정 (선택)
178
+
179
+ ```bash
180
+ # launchd plist로 Mac 부팅시 자동 시작
181
+ cat > ~/Library/LaunchAgents/com.ltcai.mlx.plist << 'EOF'
182
+ <?xml version="1.0" encoding="UTF-8"?>
183
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
184
+ <plist version="1.0">
185
+ <dict>
186
+ <key>Label</key><string>com.ltcai.mlx</string>
187
+ <key>ProgramArguments</key>
188
+ <array>
189
+ <string>/usr/bin/python3</string>
190
+ <string>/path/to/LTCAI-ai-mlx/server/server.py</string>
191
+ </array>
192
+ <key>RunAtLoad</key><true/>
193
+ <key>KeepAlive</key><true/>
194
+ </dict>
195
+ </plist>
196
+ EOF
197
+
198
+ launchctl load ~/Library/LaunchAgents/com.ltcai.mlx.plist
199
+ ```
@@ -0,0 +1,191 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import os
5
+ from pathlib import Path
6
+
7
+ import httpx
8
+ from openai import AsyncOpenAI
9
+
10
+
11
+ def load_env_file(path=".env"):
12
+ env_path = Path(path)
13
+ if not env_path.exists():
14
+ return
15
+ for raw_line in env_path.read_text(encoding="utf-8").splitlines():
16
+ line = raw_line.strip()
17
+ if not line or line.startswith("#") or "=" not in line:
18
+ continue
19
+ key, value = line.split("=", 1)
20
+ os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
21
+
22
+
23
+ load_env_file()
24
+
25
+ TELEGRAM_TOKEN = os.getenv("CODEX_TELEGRAM_BOT_TOKEN", "")
26
+ TELEGRAM_API_URL = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}"
27
+ OPENAI_MODEL = os.getenv("CODEX_OPENAI_MODEL", "gpt-5.4")
28
+ STATE_FILE = Path(os.getenv("CODEX_TELEGRAM_STATE_FILE", "codex_telegram_state.json"))
29
+ GITHUB_TOKEN = os.getenv("GITHUB_TOKEN", "")
30
+ GITHUB_REPO = os.getenv("GITHUB_REPO", "")
31
+
32
+ SYSTEM_PROMPT = """You are Lattice AI, the user's Codex development partner.
33
+ Discuss architecture, implementation plans, code reviews, and GitHub-ready changes.
34
+ Keep Korean responses concise and actionable.
35
+ When the user asks for changes that should land in the repository, propose a clear patch plan.
36
+ Use /issue to create GitHub issues when the user wants work tracked in git.
37
+ Do not claim that local Mac files were changed unless a GitHub action or explicit repository operation completed."""
38
+
39
+ logging.basicConfig(level=logging.INFO)
40
+ logger = logging.getLogger("codex_telegram_bot")
41
+ openai_client = AsyncOpenAI()
42
+
43
+
44
+ def load_state():
45
+ if not STATE_FILE.exists():
46
+ return {}
47
+ try:
48
+ return json.loads(STATE_FILE.read_text(encoding="utf-8"))
49
+ except json.JSONDecodeError:
50
+ return {}
51
+
52
+
53
+ def save_state(state):
54
+ STATE_FILE.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
55
+
56
+
57
+ async def send_message(client, chat_id, text):
58
+ chunks = [text[i:i + 3900] for i in range(0, len(text), 3900)] or [""]
59
+ for chunk in chunks:
60
+ await client.post(
61
+ f"{TELEGRAM_API_URL}/sendMessage",
62
+ json={"chat_id": chat_id, "text": chunk},
63
+ timeout=30,
64
+ )
65
+
66
+
67
+ async def get_updates(client, offset=None):
68
+ params = {"timeout": 30}
69
+ if offset is not None:
70
+ params["offset"] = offset
71
+ try:
72
+ res = await client.get(f"{TELEGRAM_API_URL}/getUpdates", params=params, timeout=35)
73
+ return res.json()
74
+ except Exception as exc:
75
+ logger.error("Telegram update failed: %s", exc)
76
+ return None
77
+
78
+
79
+ async def create_github_issue(title, body):
80
+ if not GITHUB_TOKEN or not GITHUB_REPO:
81
+ return "GITHUB_TOKEN 또는 GITHUB_REPO가 설정되지 않아 이슈를 만들 수 없습니다."
82
+
83
+ async with httpx.AsyncClient() as client:
84
+ res = await client.post(
85
+ f"https://api.github.com/repos/{GITHUB_REPO}/issues",
86
+ headers={
87
+ "Authorization": f"Bearer {GITHUB_TOKEN}",
88
+ "Accept": "application/vnd.github+json",
89
+ "X-GitHub-Api-Version": "2022-11-28",
90
+ },
91
+ json={"title": title, "body": body},
92
+ timeout=30,
93
+ )
94
+
95
+ if res.status_code >= 300:
96
+ return f"GitHub 이슈 생성 실패 ({res.status_code}): {res.text[:1000]}"
97
+
98
+ data = res.json()
99
+ return f"GitHub 이슈 생성 완료: {data.get('html_url')}"
100
+
101
+
102
+ async def ask_codex(chat_id, message):
103
+ state = load_state()
104
+ chat_key = str(chat_id)
105
+ previous_response_id = state.get(chat_key, {}).get("previous_response_id")
106
+
107
+ kwargs = {
108
+ "model": OPENAI_MODEL,
109
+ "instructions": SYSTEM_PROMPT,
110
+ "input": message,
111
+ "max_output_tokens": 1800,
112
+ }
113
+ if previous_response_id:
114
+ kwargs["previous_response_id"] = previous_response_id
115
+
116
+ response = await openai_client.responses.create(**kwargs)
117
+ state[chat_key] = {"previous_response_id": response.id}
118
+ save_state(state)
119
+ return response.output_text
120
+
121
+
122
+ def parse_issue_command(text):
123
+ content = text[len("/issue"):].strip()
124
+ if not content:
125
+ return None, None
126
+ parts = content.split("\n", 1)
127
+ title = parts[0].strip()
128
+ body = parts[1].strip() if len(parts) > 1 else title
129
+ return title, body
130
+
131
+
132
+ async def handle_message(client, chat_id, text):
133
+ if text == "/start":
134
+ await send_message(
135
+ client,
136
+ chat_id,
137
+ "Codex 전용 봇입니다.\n"
138
+ "- 일반 메시지: Codex와 개발 상담\n"
139
+ "- /reset: 이 대화 컨텍스트 초기화\n"
140
+ "- /issue 제목\\n내용: GitHub 이슈 생성",
141
+ )
142
+ return
143
+
144
+ if text == "/reset":
145
+ state = load_state()
146
+ state.pop(str(chat_id), None)
147
+ save_state(state)
148
+ await send_message(client, chat_id, "Codex 대화 컨텍스트를 초기화했습니다.")
149
+ return
150
+
151
+ if text.startswith("/issue"):
152
+ title, body = parse_issue_command(text)
153
+ if not title:
154
+ await send_message(client, chat_id, "사용법: /issue 제목\\n내용")
155
+ return
156
+ await send_message(client, chat_id, await create_github_issue(title, body))
157
+ return
158
+
159
+ await send_message(client, chat_id, "Codex가 검토 중입니다...")
160
+ try:
161
+ answer = await ask_codex(chat_id, text)
162
+ except Exception as exc:
163
+ logger.exception("OpenAI request failed")
164
+ answer = f"OpenAI 요청 실패: {exc}"
165
+ await send_message(client, chat_id, answer)
166
+
167
+
168
+ async def run_bot():
169
+ if not TELEGRAM_TOKEN:
170
+ raise RuntimeError("CODEX_TELEGRAM_BOT_TOKEN is required.")
171
+ if not os.getenv("OPENAI_API_KEY"):
172
+ raise RuntimeError("OPENAI_API_KEY is required.")
173
+
174
+ logger.info("Codex Telegram bot started.")
175
+ last_update_id = None
176
+ async with httpx.AsyncClient() as client:
177
+ while True:
178
+ updates = await get_updates(client, last_update_id)
179
+ if updates and updates.get("ok"):
180
+ for update in updates.get("result", []):
181
+ last_update_id = update["update_id"] + 1
182
+ msg = update.get("message") or {}
183
+ chat = msg.get("chat") or {}
184
+ text = msg.get("text") or ""
185
+ if chat.get("id") and text:
186
+ asyncio.create_task(handle_message(client, chat["id"], text))
187
+ await asyncio.sleep(0.5)
188
+
189
+
190
+ if __name__ == "__main__":
191
+ asyncio.run(run_bot())