Commit 019b42b5 authored by platyhouse's avatar platyhouse

# AI 도구 기능 개선 및 벡터 임베딩 스크립트 추가

## 새 스크립트: ptyAIGetVector

- ptyAIGetVector: 텍스트 벡터 임베딩 CLI 도구 추가
  - OpenAI, Google, Ollama, VoyageAI 프로바이더 지원
  - --dimensions 옵션으로 출력 벡터 차원 조절 (OpenAI text-embedding-3-* 전용)
  - --json 옵션으로 전체 요청/응답 JSON 출력
  - stdin 입력 지원 (파이프, 리다이렉션)

## AI 라이브러리 임베딩 기능 추가

- ptyLibrary_PHP/ai/chatgpt.api.common.model.php: getEmbedding() 메서드 추가 (OpenAI)
- ptyLibrary_PHP/ai/gemini.api.common.model.php: getEmbedding() 메서드 추가 (Google)
- ptyLibrary_PHP/ai/ollama.api.common.model.php: getEmbedding() 메서드 추가 (Ollama)
- ptyLibrary_PHP/ai/voyage.api.common.model.php: VoyageAI 임베딩 API 클라이언트 신규 추가
- ptyLibrary_PHP/ai/ptyAIConfig.php: VoyageAI 프로바이더 지원 및 getDefaultEmbeddingModel() 메서드 추가

## ptyAIGetMessage 개선

- ptyAIGetMessage: stdin 입력 지원 추가 (인자 없이 실행 시 stdin에서 읽기)
- ptyAIGetMessage: 도움말에 입력 방법 섹션 추가 (파이프, 리다이렉션, 클립보드)

## 문서 업데이트

- CLAUDE.md: stdin 입력 지원 패턴 가이드라인 추가
- CLAUDE.md: ptyAIGetVector 스크립트 문서화
- CLAUDE.md: getEmbedding() 메서드 및 지원 프로바이더/모델 문서화
- README.md: ptyAIGetVector 스크립트 추가 및 사용 예시
parent 8a5fd185
......@@ -254,6 +254,50 @@ if (isset($options['edit'])) {
}
```
### stdin 입력 지원 패턴
긴 텍스트를 입력받는 스크립트(쿼리, 메시지, 임베딩 텍스트 등)는 stdin 입력을 지원합니다.
**구현 패턴:**
```php
// 텍스트 입력: 인자 또는 stdin
if (empty($positionalArgs)) {
// stdin에서 읽기
if (posix_isatty(STDIN)) {
fwrite(STDERR, "입력하세요 (Ctrl+D로 완료):\n");
}
$input = file_get_contents('php://stdin');
$input = trim($input);
if (empty($input)) {
fwrite(STDERR, "Error: 입력이 필요합니다.\n");
fwrite(STDERR, "도움말: {$argv[0]} --help\n");
exit(1);
}
} else {
$input = $positionalArgs[0];
}
```
**도움말 출력 패턴:**
```php
echo "사용법: {$argv[0]} [\"텍스트\"] [옵션]\n";
echo "\n";
echo "설명...\n";
echo "텍스트 인자가 없으면 stdin에서 읽습니다 (Ctrl+D로 완료).\n";
// ...
echo "입력 방법:\n";
echo " {$argv[0]} --옵션 # stdin에서 입력 (Ctrl+D로 완료)\n";
echo " {$argv[0]} --옵션 < file.txt # 파일에서 입력 (리다이렉션)\n";
echo " cat file.txt | {$argv[0]} --옵션 # 파일에서 입력 (파이프)\n";
echo " {$argv[0]} \"\$(pbpaste)\" --옵션 # 클립보드에서 입력 (macOS)\n";
echo " {$argv[0]} \"\$(xclip -o)\" --옵션 # 클립보드에서 입력 (Linux)\n";
```
**적용된 스크립트:**
- `ptyAIGetMessage` - AI 메시지 전송
- `ptyAIGetVector` - 벡터 임베딩
- `ptyMysqlQuery` - SQL 쿼리 실행
### x_ 접두사 무시 패턴 (선택)
데이터베이스/테이블/인덱스 등에서 `x_` 접두사 항목을 무시하는 옵션:
......@@ -1084,11 +1128,12 @@ try {
## ptyAIGetMessage - AI API CLI 도구
다양한 AI API(Claude, ChatGPT, Gemini, Ollama)를 통해 메시지를 전송하고 응답을 받는 CLI 도구입니다.
메시지 인자가 없으면 stdin에서 읽습니다 (Ctrl+D로 완료).
### 기본 사용법
```bash
./ptyAIGetMessage.php "메시지" [옵션]
./ptyAIGetMessage ["메시지"] [옵션]
```
### CLI 옵션
......@@ -1106,22 +1151,41 @@ try {
```bash
# 기본 사용
./ptyAIGetMessage.php "안녕하세요"
./ptyAIGetMessage "안녕하세요"
# 특정 섹션 사용
./ptyAIGetMessage.php "안녕" --ai=openai
./ptyAIGetMessage "안녕" --ai=openai
# 모델 오버라이드
./ptyAIGetMessage.php "안녕" --ai=anthropic --model=claude-3-haiku-20240307
./ptyAIGetMessage "안녕" --ai=anthropic --model=claude-3-haiku-20240307
# Claude 베타 기능
./ptyAIGetMessage.php "안녕" --ai=claude --anthropic-beta=context-1m-2025-08-07
./ptyAIGetMessage "안녕" --ai=claude --anthropic-beta=context-1m-2025-08-07
# 상세 정보 출력 (curl 요청/응답 포함)
./ptyAIGetMessage.php "안녕" --ai=openai --verbose
./ptyAIGetMessage "안녕" --ai=openai --verbose
# JSON 출력
./ptyAIGetMessage.php "안녕" --ai=google --json
./ptyAIGetMessage "안녕" --ai=google --json
```
### 입력 방법
```bash
# stdin에서 입력 (Ctrl+D로 완료)
./ptyAIGetMessage --ai=openai
# 파일에서 입력 (리다이렉션)
./ptyAIGetMessage --ai=openai < prompt.txt
# 파일에서 입력 (파이프)
cat prompt.txt | ./ptyAIGetMessage --ai=openai
# 클립보드에서 입력 (macOS)
./ptyAIGetMessage "$(pbpaste)" --ai=openai
# 클립보드에서 입력 (Linux)
./ptyAIGetMessage "$(xclip -o)" --ai=openai
```
### 설정 파일: ~/.ptyAIConfig.ini
......@@ -1240,6 +1304,34 @@ $client->setAnthropicBeta($feature); // anthropic-beta 헤더 설정
$client->setApiUrl($url); // API URL 설정
```
### 임베딩 메서드 (getEmbedding)
OpenAI, Google, Ollama, VoyageAI 클라이언트에서 텍스트 임베딩을 얻을 수 있습니다.
```php
// OpenAI (dimensions 옵션 지원)
$client = ptyAIConfig::createClient('openai');
$result = $client->getEmbedding($text, 'text-embedding-3-small', 512);
// Google Gemini
$client = ptyAIConfig::createClient('google');
$result = $client->getEmbedding($text, 'text-embedding-004');
// Ollama
$client = ptyAIConfig::createClient('ollama');
$result = $client->getEmbedding($text, 'nomic-embed-text');
// VoyageAI (inputType 옵션 지원: 'query' 또는 'document')
$client = ptyAIConfig::createClient('voyageai');
$result = $client->getEmbedding($text, 'voyage-3-large', 'query');
// 결과
$embedding = $result['embedding']; // 벡터 배열
$usage = $result['usage']; // 토큰 사용량 (OpenAI, VoyageAI)
```
**참고:** `anthropic`(Claude)는 임베딩 API를 지원하지 않습니다.
### 토큰 사용량 응답 위치
| Provider | 입력 토큰 | 출력 토큰 |
......@@ -1249,6 +1341,124 @@ $client->setApiUrl($url); // API URL 설정
| google | `usageMetadata.promptTokenCount` | `usageMetadata.candidatesTokenCount` |
| ollama | `prompt_eval_count` | `eval_count` |
## ptyAIGetVector - 벡터 임베딩 CLI 도구
텍스트를 벡터 임베딩으로 변환하는 CLI 도구입니다.
텍스트 인자가 없으면 stdin에서 읽습니다 (Ctrl+D로 완료).
### 기본 사용법
```bash
./ptyAIGetVector ["텍스트"] [옵션]
```
### CLI 옵션
| 옵션 | 설명 | 기본값 |
|------|------|--------|
| `--ai=섹션명` | INI 파일 섹션 | `default` |
| `--model=모델명` | 임베딩 모델 오버라이드 | 프로바이더별 기본값 |
| `--dimensions=N` | 출력 벡터 차원 수 (OpenAI text-embedding-3-* 전용) | - |
| `--verbose` | 상세 정보 출력 | - |
| `--json` | 요청/응답 전체를 JSON으로 출력 | - |
| `--edit` | 설정 파일을 에디터로 열기 | - |
| `--help` | 도움말 출력 | - |
### 지원 프로바이더 및 모델
| Provider | 기본 모델 | 차원 | 비고 |
|----------|----------|------|------|
| `openai` | text-embedding-3-small | 1536 | 저렴, dimensions 옵션 지원 |
| `openai` | text-embedding-3-large | 3072 | 고성능, dimensions 옵션 지원 |
| `openai` | text-embedding-ada-002 | 1536 | 레거시 |
| `google` | text-embedding-004 | 768 | Gemini |
| `ollama` | nomic-embed-text | 768 | 로컬 |
| `ollama` | mxbai-embed-large | 1024 | 로컬, 고성능 |
| `ollama` | all-minilm | 384 | 경량 |
| `voyageai` | voyage-3-large | 1024 | 고성능 |
| `voyageai` | voyage-3 | 1024 | 표준 |
| `voyageai` | voyage-3-lite | 512 | 경량 |
| `voyageai` | voyage-code-3 | 1024 | 코드 특화 |
**주의:** `anthropic`(Claude)는 임베딩 API를 지원하지 않습니다.
### 사용 예시
```bash
# 기본 설정으로 임베딩
./ptyAIGetVector "안녕하세요"
# OpenAI 사용
./ptyAIGetVector "Hello" --ai=openai
# 고성능 모델 사용
./ptyAIGetVector "Hello" --ai=openai --model=text-embedding-3-large
# 차원 축소 (512차원)
./ptyAIGetVector "Hello" --ai=openai --dimensions=512
# Ollama 로컬 임베딩
./ptyAIGetVector "테스트" --ai=ollama --verbose
# VoyageAI 사용
./ptyAIGetVector "테스트" --ai=voyageai
# 전체 JSON 출력
./ptyAIGetVector "테스트" --json
```
### 입력 방법
```bash
# stdin에서 입력 (Ctrl+D로 완료)
./ptyAIGetVector --ai=voyageai
# 파일에서 입력 (리다이렉션)
./ptyAIGetVector --ai=voyageai < document.txt
# 파일에서 입력 (파이프)
cat document.txt | ./ptyAIGetVector --ai=voyageai
# 클립보드에서 입력 (macOS)
./ptyAIGetVector "$(pbpaste)" --ai=voyageai
# 클립보드에서 입력 (Linux)
./ptyAIGetVector "$(xclip -o)" --ai=voyageai
```
### 출력 형식
**기본 출력:**
```json
[0.123, -0.456, 0.789, ...]
```
**--json 출력:**
```json
{
"request": {
"provider": "openai",
"model": "text-embedding-3-small",
"text": "안녕하세요"
},
"response": {
"embedding": [0.123, -0.456, ...],
"dimensions": 1536,
"elapsed_ms": 234,
"tokens": 5
}
}
```
### API 엔드포인트
| Provider | Endpoint |
|----------|----------|
| openai | `POST https://api.openai.com/v1/embeddings` |
| google | `POST https://generativelanguage.googleapis.com/v1beta/models/{model}:embedContent` |
| ollama | `POST http://localhost:11434/api/embeddings` |
| voyageai | `POST https://api.voyageai.com/v1/embeddings` |
## ptyGit* 스크립트
Git 관련 CLI 도구 모음입니다.
......
......@@ -145,6 +145,7 @@ apiUrl=http://localhost:11434
| 스크립트 | 설명 |
|---------|------|
| `ptyAIGetMessage` | AI API 메시지 전송 (Claude, GPT, Gemini, Ollama) |
| `ptyAIGetVector` | 텍스트 벡터 임베딩 (OpenAI, Gemini, Ollama, VoyageAI) |
| `ptyAIStatistics` | AI 사용량 통계 |
### Git 도구
......@@ -242,6 +243,18 @@ ptyAIGetMessage "Hello" --ai=openai
# JSON 출력
ptyAIGetMessage "테스트" --json
# 벡터 임베딩 (OpenAI)
ptyAIGetVector "안녕하세요" --ai=openai
# 벡터 임베딩 + 차원 지정
ptyAIGetVector "Hello" --ai=openai --dimensions=512
# Ollama 로컬 임베딩
ptyAIGetVector "테스트" --ai=ollama
# VoyageAI 임베딩
ptyAIGetVector "테스트" --ai=voyageai
```
### 크론 + 중복 실행 제어
......
......@@ -37,9 +37,12 @@ if (isset($options['edit'])) {
exit(0);
}
// 도움말 또는 필수 인자 확인
if (empty($positionalArgs) || isset($options['help'])) {
fwrite(STDERR, "사용법: {$argv[0]} \"메시지\" [옵션]\n");
// 도움말
if (isset($options['help'])) {
fwrite(STDERR, "사용법: {$argv[0]} [\"메시지\"] [옵션]\n");
fwrite(STDERR, "\n");
fwrite(STDERR, "AI API를 통해 메시지를 전송하고 응답을 받습니다.\n");
fwrite(STDERR, "메시지 인자가 없으면 stdin에서 읽습니다 (Ctrl+D로 완료).\n");
fwrite(STDERR, "\n");
fwrite(STDERR, "옵션:\n");
fwrite(STDERR, " --ai=섹션명 INI 파일 섹션 (기본값: default)\n");
......@@ -59,6 +62,13 @@ if (empty($positionalArgs) || isset($options['help'])) {
fwrite(STDERR, "anthropic-beta 예시:\n");
fwrite(STDERR, " context-1m-2025-08-07, interleaved-thinking-2025-05-14, max-tokens-3-5-sonnet-2024-07-15\n");
fwrite(STDERR, "\n");
fwrite(STDERR, "입력 방법:\n");
fwrite(STDERR, " {$argv[0]} --ai=openai # stdin에서 입력 (Ctrl+D로 완료)\n");
fwrite(STDERR, " {$argv[0]} --ai=openai < prompt.txt # 파일에서 입력 (리다이렉션)\n");
fwrite(STDERR, " cat prompt.txt | {$argv[0]} --ai=openai # 파일에서 입력 (파이프)\n");
fwrite(STDERR, " {$argv[0]} \"\$(pbpaste)\" --ai=openai # 클립보드에서 입력 (macOS)\n");
fwrite(STDERR, " {$argv[0]} \"\$(xclip -o)\" --ai=openai # 클립보드에서 입력 (Linux)\n");
fwrite(STDERR, "\n");
// 현재 설정 파일 내용 표시
$configPath = ptyAIConfig::getConfigPath();
......@@ -93,10 +103,25 @@ if (empty($positionalArgs) || isset($options['help'])) {
fwrite(STDERR, "\n");
fwrite(STDERR, ptyAIConfig::getConfigExample() . "\n");
}
exit(isset($options['help']) ? 0 : 1);
exit(0);
}
$message = $positionalArgs[0];
// 메시지 입력: 인자 또는 stdin
if (empty($positionalArgs)) {
// stdin에서 읽기
if (posix_isatty(STDIN)) {
fwrite(STDERR, "메시지를 입력하세요 (Ctrl+D로 완료):\n");
}
$message = file_get_contents('php://stdin');
$message = trim($message);
if (empty($message)) {
fwrite(STDERR, "Error: 메시지가 필요합니다.\n");
fwrite(STDERR, "도움말: {$argv[0]} --help\n");
exit(1);
}
} else {
$message = $positionalArgs[0];
}
try {
// AI 클라이언트 연결
......
#!/usr/bin/env php
<?php
/**
* ptyAIGetVector
*
* AI API를 통해 텍스트의 벡터 임베딩을 얻는 CLI 도구
*
* 설정 파일: ~/.ptyAIConfig.ini
*
* Usage: ./ptyAIGetVector "텍스트" [--ai=섹션명] [--model=모델명]
*
* 지원 provider:
* - openai: text-embedding-3-small, text-embedding-3-large, text-embedding-ada-002
* - google: text-embedding-004
* - ollama: nomic-embed-text, mxbai-embed-large, all-minilm
*
* 참고: anthropic(Claude)는 임베딩 API를 제공하지 않습니다.
*/
namespace platyFramework;
require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliOptionParser.php';
require_once __DIR__ . '/ptyLibrary_PHP/ai/ptyAIConfig.php';
// 인자 파싱
$parsed = ptyCliOptionParser::parse($argv);
$positionalArgs = $parsed['positional'];
$options = $parsed['options'];
$aiSection = isset($options['ai']) ? $options['ai'] : 'default';
$modelOverride = isset($options['model']) ? $options['model'] : null;
$verbose = isset($options['verbose']);
$jsonOutput = isset($options['json']);
$dimensions = isset($options['dimensions']) ? intval($options['dimensions']) : null;
// --edit 옵션: vi로 설정 파일 열기
if (isset($options['edit'])) {
$editor = getenv('EDITOR') ? getenv('EDITOR') : 'vi';
$configPath = ptyAIConfig::getConfigPath();
passthru("$editor " . escapeshellarg($configPath));
exit(0);
}
// 도움말
if (isset($options['help'])) {
printHelp($argv[0]);
exit(0);
}
// 텍스트 입력: 인자 또는 stdin
if (empty($positionalArgs)) {
// stdin에서 읽기
if (posix_isatty(STDIN)) {
fwrite(STDERR, "텍스트를 입력하세요 (Ctrl+D로 완료):\n");
}
$text = file_get_contents('php://stdin');
$text = trim($text);
if (empty($text)) {
fwrite(STDERR, "Error: 텍스트가 필요합니다.\n");
fwrite(STDERR, "도움말: {$argv[0]} --help\n");
exit(1);
}
} else {
$text = $positionalArgs[0];
}
try {
// AI 설정 로드
$config = ptyAIConfig::load($aiSection);
$provider = $config['provider'];
// anthropic은 임베딩 API 미지원
if ($provider === 'anthropic') {
fwrite(STDERR, "Error: anthropic(Claude)는 임베딩 API를 지원하지 않습니다.\n");
fwrite(STDERR, "openai, google, ollama 섹션을 사용하세요.\n");
exit(1);
}
// 클라이언트 생성
$client = ptyAIConfig::createClient($aiSection);
// 디버그 모드 설정
if ($verbose && method_exists($client, 'setDebug')) {
$client->setDebug(true);
}
// 임베딩 모델 결정
$model = $modelOverride ? $modelOverride : ptyAIConfig::getDefaultEmbeddingModel($provider);
if ($verbose) {
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
fwrite(STDERR, "Provider: {$provider}\n");
fwrite(STDERR, "Model: {$model}\n");
fwrite(STDERR, "Section: {$aiSection}\n");
if ($dimensions) {
fwrite(STDERR, "Dimensions: {$dimensions}\n");
}
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
fwrite(STDERR, "Text: " . mb_substr($text, 0, 100) . (mb_strlen($text) > 100 ? '...' : '') . "\n");
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
fwrite(STDERR, "API 호출 중...\n");
}
$startTime = microtime(true);
// 임베딩 API 호출 (클래스 메서드 사용)
if ($provider === 'openai') {
$result = $client->getEmbedding($text, $model, $dimensions);
} else {
$result = $client->getEmbedding($text, $model);
}
$elapsed = round((microtime(true) - $startTime) * 1000);
if ($result === false) {
fwrite(STDERR, "Error: 임베딩 API 호출에 실패했습니다.\n");
exit(1);
}
$embedding = $result['embedding'];
$response = $result['response'];
$usage = isset($result['usage']) ? $result['usage'] : null;
if ($verbose) {
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
fwrite(STDERR, "응답 시간: {$elapsed}ms\n");
fwrite(STDERR, "벡터 차원: " . count($embedding) . "\n");
if ($usage) {
fwrite(STDERR, "토큰 사용량: {$usage} tokens\n");
}
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
}
// 출력
if ($jsonOutput) {
$output = array(
'request' => array(
'provider' => $provider,
'model' => $model,
'text' => $text,
),
'response' => array(
'embedding' => $embedding,
'dimensions' => count($embedding),
'elapsed_ms' => $elapsed,
),
);
if ($dimensions) {
$output['request']['dimensions'] = $dimensions;
}
if ($usage) {
$output['response']['tokens'] = $usage;
}
echo json_encode($output, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
} else {
// 벡터만 출력 (JSON 배열)
echo json_encode($response) . "\n";
}
} catch (\Exception $e) {
fwrite(STDERR, "Error: " . $e->getMessage() . "\n");
exit(1);
}
/**
* 도움말 출력
*/
function printHelp($scriptName)
{
fwrite(STDERR, "사용법: {$scriptName} [\"텍스트\"] [옵션]\n");
fwrite(STDERR, "\n");
fwrite(STDERR, "텍스트를 벡터 임베딩으로 변환합니다.\n");
fwrite(STDERR, "텍스트 인자가 없으면 stdin에서 읽습니다 (Ctrl+D로 완료).\n");
fwrite(STDERR, "\n");
fwrite(STDERR, "옵션:\n");
fwrite(STDERR, " --ai=섹션명 INI 파일 섹션 (기본값: default)\n");
fwrite(STDERR, " --model=모델명 임베딩 모델 오버라이드\n");
fwrite(STDERR, " --dimensions=N 출력 벡터 차원 수 (OpenAI text-embedding-3-* 전용)\n");
fwrite(STDERR, " --verbose 상세 정보 출력\n");
fwrite(STDERR, " --json 요청/응답 전체를 JSON으로 출력\n");
fwrite(STDERR, " --edit 설정 파일을 에디터로 열기\n");
fwrite(STDERR, " --help 도움말 출력\n");
fwrite(STDERR, "\n");
fwrite(STDERR, "지원 프로바이더 및 모델:\n");
fwrite(STDERR, " openai:\n");
fwrite(STDERR, " - text-embedding-3-small (기본값, 1536차원, 저렴)\n");
fwrite(STDERR, " - text-embedding-3-large (3072차원, 고성능)\n");
fwrite(STDERR, " - text-embedding-ada-002 (1536차원, 레거시)\n");
fwrite(STDERR, "\n");
fwrite(STDERR, " google:\n");
fwrite(STDERR, " - text-embedding-004 (기본값, 768차원)\n");
fwrite(STDERR, "\n");
fwrite(STDERR, " ollama:\n");
fwrite(STDERR, " - nomic-embed-text (기본값, 768차원)\n");
fwrite(STDERR, " - mxbai-embed-large (1024차원)\n");
fwrite(STDERR, " - all-minilm (384차원, 경량)\n");
fwrite(STDERR, "\n");
fwrite(STDERR, " voyageai:\n");
fwrite(STDERR, " - voyage-3-large (기본값, 1024차원, 고성능)\n");
fwrite(STDERR, " - voyage-3 (1024차원)\n");
fwrite(STDERR, " - voyage-3-lite (512차원, 경량)\n");
fwrite(STDERR, " - voyage-code-3 (1024차원, 코드 특화)\n");
fwrite(STDERR, "\n");
fwrite(STDERR, "주의: anthropic(Claude)는 임베딩 API를 지원하지 않습니다.\n");
fwrite(STDERR, "\n");
fwrite(STDERR, "예시:\n");
fwrite(STDERR, " {$scriptName} \"안녕하세요\" # 기본 설정으로 임베딩\n");
fwrite(STDERR, " {$scriptName} \"Hello\" --ai=openai # OpenAI 사용\n");
fwrite(STDERR, " {$scriptName} \"Hello\" --ai=openai --model=text-embedding-3-large\n");
fwrite(STDERR, " {$scriptName} \"Hello\" --ai=openai --dimensions=512 # 차원 축소\n");
fwrite(STDERR, " {$scriptName} \"테스트\" --ai=ollama --verbose # Ollama + 상세 로그\n");
fwrite(STDERR, " {$scriptName} \"테스트\" --ai=voyageai # VoyageAI 사용\n");
fwrite(STDERR, " {$scriptName} \"테스트\" --json # 전체 JSON 출력\n");
fwrite(STDERR, "\n");
fwrite(STDERR, "입력 방법:\n");
fwrite(STDERR, " {$scriptName} --ai=voyageai # stdin에서 입력 (Ctrl+D로 완료)\n");
fwrite(STDERR, " {$scriptName} --ai=voyageai < file.txt # 파일에서 입력 (리다이렉션)\n");
fwrite(STDERR, " cat file.txt | {$scriptName} --ai=voyageai # 파일에서 입력 (파이프)\n");
fwrite(STDERR, " {$scriptName} \"\$(pbpaste)\" --ai=voyageai # 클립보드에서 입력 (macOS)\n");
fwrite(STDERR, " {$scriptName} \"\$(xclip -o)\" --ai=voyageai # 클립보드에서 입력 (Linux)\n");
fwrite(STDERR, "\n");
fwrite(STDERR, "설정 파일: ~/.ptyAIConfig.ini\n");
fwrite(STDERR, ptyAIConfig::getConfigExample() . "\n");
}
......@@ -281,4 +281,114 @@ class ChatGPTAPIModel extends model
$response = $this->get($message);
return $this->extractText($response);
}
/**
* 텍스트 임베딩 API 호출
* @param string $text 임베딩할 텍스트
* @param string $model 임베딩 모델 (기본값: text-embedding-3-small)
* @param int|null $dimensions 출력 차원 수 (text-embedding-3-* 전용)
* @return array|false ['embedding' => [...], 'usage' => N] 또는 false
*/
public function getEmbedding($text, $model = null, $dimensions = null)
{
if ($model === null) {
$model = 'text-embedding-3-small';
}
$url = 'https://api.openai.com/v1/embeddings';
$data = array(
'input' => $text,
'model' => $model,
);
// text-embedding-3-* 모델은 dimensions 지원
if ($dimensions !== null && strpos($model, 'text-embedding-3') === 0) {
$data['dimensions'] = $dimensions;
}
$headers = array(
'Content-Type: application/json',
'Authorization: Bearer ' . $this->apiKey,
);
// 디버그 출력
if ($this->debug) {
fwrite(STDERR, "━━━━━━━━━━━━ CURL REQUEST ━━━━━━━━━━━━\n");
fwrite(STDERR, "URL: {$url}\n");
fwrite(STDERR, "Method: POST\n");
fwrite(STDERR, "Headers:\n");
foreach ($headers as $header) {
if (strpos($header, 'Authorization:') === 0) {
$key = substr($this->apiKey, 0, 12) . '...';
fwrite(STDERR, " Authorization: Bearer {$key}\n");
} else {
fwrite(STDERR, " {$header}\n");
}
}
fwrite(STDERR, "Body:\n");
fwrite(STDERR, " " . json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n");
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
}
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
// 디버그 출력 (응답)
if ($this->debug) {
fwrite(STDERR, "━━━━━━━━━━━━ CURL RESPONSE ━━━━━━━━━━━━\n");
fwrite(STDERR, "HTTP Code: {$httpCode}\n");
if ($error) {
fwrite(STDERR, "cURL Error: {$error}\n");
}
fwrite(STDERR, "Response:\n");
$prettyResponse = json_decode($response, true);
if ($prettyResponse) {
if (isset($prettyResponse['data'][0]['embedding'])) {
$prettyResponse['data'][0]['embedding'] = '[' . count($prettyResponse['data'][0]['embedding']) . ' dimensions...]';
}
fwrite(STDERR, " " . json_encode($prettyResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n");
} else {
fwrite(STDERR, " {$response}\n");
}
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
}
if ($error) {
error_log("OpenAI Embedding API cURL Error: " . $error);
return false;
}
if ($httpCode !== 200) {
error_log("OpenAI Embedding API HTTP Error: " . $httpCode . " - " . $response);
return false;
}
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("OpenAI Embedding API JSON Decode Error: " . json_last_error_msg());
return false;
}
if (!isset($result['data'][0]['embedding'])) {
return false;
}
return array(
'response' => $result,
'embedding' => $result['data'][0]['embedding'],
'usage' => isset($result['usage']['total_tokens']) ? $result['usage']['total_tokens'] : null,
'_raw' => $result,
);
}
}
\ No newline at end of file
......@@ -214,4 +214,72 @@ class GeminiAPIModel extends model
$response = $this->get($message);
return $this->extractText($response);
}
/**
* 텍스트 임베딩 API 호출
* @param string $text 임베딩할 텍스트
* @param string $model 임베딩 모델 (기본값: text-embedding-004)
* @return array|false ['embedding' => [...], 'usage' => null] 또는 false
*/
public function getEmbedding($text, $model = null)
{
if ($model === null) {
$model = 'text-embedding-004';
}
$url = "{$this->apiBaseUrl}/{$model}:embedContent?key={$this->apiKey}";
$data = array(
'model' => "models/{$model}",
'content' => array(
'parts' => array(
array('text' => $text)
)
)
);
$headers = array(
'Content-Type: application/json',
);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
error_log("Gemini Embedding API cURL Error: " . $error);
return false;
}
if ($httpCode !== 200) {
error_log("Gemini Embedding API HTTP Error: " . $httpCode . " - " . $response);
return false;
}
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("Gemini Embedding API JSON Decode Error: " . json_last_error_msg());
return false;
}
if (!isset($result['embedding']['values'])) {
return false;
}
return array(
'response' => $result,
'embedding' => $result['embedding']['values'],
'usage' => null, // Gemini doesn't return token usage for embeddings
'_raw' => $result,
);
}
}
......@@ -265,4 +265,103 @@ class OllamaAPIModel extends model
$response = $this->get($message);
return $this->extractText($response);
}
/**
* 텍스트 임베딩 API 호출
* @param string $text 임베딩할 텍스트
* @param string $model 임베딩 모델 (기본값: nomic-embed-text)
* @return array|false ['embedding' => [...], 'usage' => null] 또는 false
*/
public function getEmbedding($text, $model = null)
{
if ($model === null) {
$model = 'nomic-embed-text';
}
$url = $this->apiUrl . '/api/embeddings';
$data = array(
'model' => $model,
'prompt' => $text,
);
$headers = array(
'Content-Type: application/json',
);
// 디버그 출력
if ($this->debug) {
fwrite(STDERR, "━━━━━━━━━━━━ CURL REQUEST ━━━━━━━━━━━━\n");
fwrite(STDERR, "URL: {$url}\n");
fwrite(STDERR, "Method: POST\n");
fwrite(STDERR, "Headers:\n");
foreach ($headers as $header) {
fwrite(STDERR, " {$header}\n");
}
fwrite(STDERR, "Body:\n");
fwrite(STDERR, " " . json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n");
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
}
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 300);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
// 디버그 출력 (응답)
if ($this->debug) {
fwrite(STDERR, "━━━━━━━━━━━━ CURL RESPONSE ━━━━━━━━━━━━\n");
fwrite(STDERR, "HTTP Code: {$httpCode}\n");
if ($error) {
fwrite(STDERR, "cURL Error: {$error}\n");
}
fwrite(STDERR, "Response:\n");
$prettyResponse = json_decode($response, true);
if ($prettyResponse) {
if (isset($prettyResponse['embedding'])) {
$prettyResponse['embedding'] = '[' . count($prettyResponse['embedding']) . ' dimensions...]';
}
fwrite(STDERR, " " . json_encode($prettyResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n");
} else {
fwrite(STDERR, " {$response}\n");
}
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
}
if ($error) {
error_log("Ollama Embedding API cURL Error: " . $error);
return false;
}
if ($httpCode !== 200) {
error_log("Ollama Embedding API HTTP Error: " . $httpCode . " - " . $response);
return false;
}
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("Ollama Embedding API JSON Decode Error: " . json_last_error_msg());
return false;
}
if (!isset($result['embedding'])) {
return false;
}
return array(
'response' => $result,
'embedding' => $result['embedding'],
'usage' => null, // Ollama doesn't return token usage for embeddings
'_raw' => $result,
);
}
}
......@@ -17,6 +17,7 @@ require_once __DIR__ . "/claude.api.common.model.php";
require_once __DIR__ . "/chatgpt.api.common.model.php";
require_once __DIR__ . "/gemini.api.common.model.php";
require_once __DIR__ . "/ollama.api.common.model.php";
require_once __DIR__ . "/voyage.api.common.model.php";
/**
* AI 설정 로더 클래스
......@@ -92,7 +93,7 @@ class ptyAIConfig
throw new \Exception("AI 설정 [{$section}] 섹션에 필수 필드 'apiKey'가 없습니다.");
}
}
$validProviders = ['anthropic', 'openai', 'google', 'ollama'];
$validProviders = ['anthropic', 'openai', 'google', 'ollama', 'voyageai'];
if (!in_array($provider, $validProviders)) {
throw new \Exception("AI 설정 [{$section}] 섹션의 provider '{$provider}'는 지원되지 않습니다.\n지원되는 provider: " . implode(', ', $validProviders));
}
......@@ -122,8 +123,23 @@ class ptyAIConfig
'openai' => 'gpt-4o',
'google' => 'gemini-2.0-flash-exp',
'ollama' => 'llama3',
'voyageai' => 'voyage-3-large', // 임베딩 전용
];
return $defaults[$provider] ?? null;
return isset($defaults[$provider]) ? $defaults[$provider] : null;
}
/**
* provider별 기본 임베딩 모델 반환
*/
public static function getDefaultEmbeddingModel($provider)
{
$defaults = [
'openai' => 'text-embedding-3-small',
'google' => 'text-embedding-004',
'ollama' => 'nomic-embed-text',
'voyageai' => 'voyage-3-large',
];
return isset($defaults[$provider]) ? $defaults[$provider] : null;
}
/**
......@@ -165,6 +181,11 @@ class ptyAIConfig
}
return $client;
case 'voyageai':
$client = new VoyageAPIModel($config['apiKey']);
$client->setModel($config['model']);
return $client;
default:
throw new \Exception("지원되지 않는 provider: {$config['provider']}");
}
......@@ -221,9 +242,15 @@ provider=ollama
model=llama3
apiUrl=http://localhost:11434
# 지원 provider: anthropic, openai, google, ollama
[voyageai]
provider=voyageai
model=voyage-3-large
apiKey=your_voyageai_api_key
# 지원 provider: anthropic, openai, google, ollama, voyageai
# anthropic-beta: Claude 베타 기능 사용 시 헤더값 (예: context-1m-2025-08-07)
# apiUrl: ollama 서버 URL (기본값: http://localhost:11434)
# voyageai: 임베딩 전용 API (voyage-3-large, voyage-3, voyage-3-lite, voyage-code-3 등)
EOT;
}
}
<?php
namespace platyFramework;
require_once(__DIR__ . "/../ptycommon/model.php");
/**
* VoyageAI API Model
* VoyageAI 임베딩 API를 사용하기 위한 클래스
*
* 참고: VoyageAI는 임베딩 전용 API입니다. 텍스트 생성(get/getSimple)은 지원하지 않습니다.
*/
class VoyageAPIModel extends model
{
private $apiKey;
private $model = 'voyage-3-large';
private $apiUrl = 'https://api.voyageai.com/v1/embeddings';
private $debug = false;
/**
* 생성자
* @param string $apiKey VoyageAI API 키
*/
public function __construct($apiKey = null)
{
parent::__construct();
$this->apiKey = $apiKey;
}
/**
* API 키 설정
* @param string $apiKey
* @return $this
*/
public function setAPIKey($apiKey)
{
$this->apiKey = $apiKey;
return $this;
}
/**
* 모델 설정
* @param string $model 사용할 VoyageAI 모델명
* @return $this
*/
public function setModel($model)
{
$this->model = $model;
return $this;
}
/**
* 디버그 모드 설정
* @param bool $debug 디버그 모드 활성화 여부
* @return $this
*/
public function setDebug($debug)
{
$this->debug = $debug;
return $this;
}
/**
* 텍스트 생성 API (미지원)
* VoyageAI는 임베딩 전용 API이므로 텍스트 생성을 지원하지 않습니다.
*
* @param string $message
* @return false
*/
public function get($message)
{
error_log("VoyageAI는 텍스트 생성을 지원하지 않습니다. getEmbedding()을 사용하세요.");
return false;
}
/**
* 간단한 텍스트 응답 (미지원)
* @param string $message
* @return false
*/
public function getSimple($message)
{
return false;
}
/**
* 응답에서 텍스트 추출 (미지원)
* @param array $response
* @return false
*/
public function extractText($response)
{
return false;
}
/**
* 텍스트 임베딩 API 호출
* @param string|array $text 임베딩할 텍스트 (단일 문자열 또는 배열)
* @param string $model 임베딩 모델 (기본값: voyage-3-large)
* @param string|null $inputType 입력 유형 (query, document, null)
* @return array|false ['embedding' => [...], 'usage' => N] 또는 false
*/
public function getEmbedding($text, $model = null, $inputType = null)
{
if ($model === null) {
$model = $this->model;
}
// 단일 문자열을 배열로 변환
$input = is_array($text) ? $text : array($text);
$data = array(
'input' => $input,
'model' => $model,
);
// input_type 추가 (query: 검색 쿼리, document: 저장할 문서)
if ($inputType !== null) {
$data['input_type'] = $inputType;
}
$headers = array(
'Content-Type: application/json',
'Authorization: Bearer ' . $this->apiKey,
);
// 디버그 출력
if ($this->debug) {
fwrite(STDERR, "━━━━━━━━━━━━ CURL REQUEST ━━━━━━━━━━━━\n");
fwrite(STDERR, "URL: {$this->apiUrl}\n");
fwrite(STDERR, "Method: POST\n");
fwrite(STDERR, "Headers:\n");
foreach ($headers as $header) {
if (strpos($header, 'Authorization:') === 0) {
$key = substr($this->apiKey, 0, 12) . '...';
fwrite(STDERR, " Authorization: Bearer {$key}\n");
} else {
fwrite(STDERR, " {$header}\n");
}
}
fwrite(STDERR, "Body:\n");
fwrite(STDERR, " " . json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n");
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
}
$ch = curl_init($this->apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
// 디버그 출력 (응답)
if ($this->debug) {
fwrite(STDERR, "━━━━━━━━━━━━ CURL RESPONSE ━━━━━━━━━━━━\n");
fwrite(STDERR, "HTTP Code: {$httpCode}\n");
if ($error) {
fwrite(STDERR, "cURL Error: {$error}\n");
}
fwrite(STDERR, "Response:\n");
$prettyResponse = json_decode($response, true);
if ($prettyResponse) {
if (isset($prettyResponse['data'][0]['embedding'])) {
$prettyResponse['data'][0]['embedding'] = '[' . count($prettyResponse['data'][0]['embedding']) . ' dimensions...]';
}
fwrite(STDERR, " " . json_encode($prettyResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n");
} else {
fwrite(STDERR, " {$response}\n");
}
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
}
if ($error) {
error_log("VoyageAI Embedding API cURL Error: " . $error);
return false;
}
if ($httpCode !== 200) {
error_log("VoyageAI Embedding API HTTP Error: " . $httpCode . " - " . $response);
return false;
}
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("VoyageAI Embedding API JSON Decode Error: " . json_last_error_msg());
return false;
}
if (!isset($result['data'][0]['embedding'])) {
return false;
}
return array(
'response' => $result,
'embedding' => $result['data'][0]['embedding'],
'usage' => isset($result['usage']['total_tokens']) ? $result['usage']['total_tokens'] : null,
'_raw' => $result,
);
}
/**
* 여러 텍스트의 임베딩을 한 번에 얻기
* @param array $texts 임베딩할 텍스트 배열
* @param string $model 임베딩 모델
* @param string|null $inputType 입력 유형
* @return array|false ['embeddings' => [[...], [...]], 'usage' => N] 또는 false
*/
public function getEmbeddings($texts, $model = null, $inputType = null)
{
if ($model === null) {
$model = $this->model;
}
$data = array(
'input' => $texts,
'model' => $model,
);
if ($inputType !== null) {
$data['input_type'] = $inputType;
}
$headers = array(
'Content-Type: application/json',
'Authorization: Bearer ' . $this->apiKey,
);
$ch = curl_init($this->apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
error_log("VoyageAI Embedding API cURL Error: " . $error);
return false;
}
if ($httpCode !== 200) {
error_log("VoyageAI Embedding API HTTP Error: " . $httpCode . " - " . $response);
return false;
}
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("VoyageAI Embedding API JSON Decode Error: " . json_last_error_msg());
return false;
}
if (!isset($result['data'])) {
return false;
}
$embeddings = array();
foreach ($result['data'] as $item) {
$embeddings[] = $item['embedding'];
}
return array(
'response' => $result,
'embeddings' => $embeddings,
'usage' => isset($result['usage']['total_tokens']) ? $result['usage']['total_tokens'] : null,
'_raw' => $result,
);
}
}
......@@ -30,10 +30,10 @@ $showMarkdown = isset($options['markdown']);
// 1. --help: 최우선 처리
// ============================================================
if (isset($options['help'])) {
echo "사용법: {$argv[0]} \"SQL\" [옵션]\n";
echo "사용법: {$argv[0]} [\"SQL\"] [옵션]\n";
echo "\n";
echo "인자:\n";
echo " SQL 실행할 SQL 쿼리\n";
echo "MySQL 쿼리를 실행하고 결과를 출력합니다.\n";
echo "SQL 인자가 없으면 stdin에서 읽습니다 (Ctrl+D로 완료).\n";
echo "\n";
echo "옵션:\n";
echo " --mysql=섹션명 INI 파일 섹션 (기본값: default)\n";
......@@ -59,6 +59,13 @@ if (isset($options['help'])) {
echo " {$argv[0]} \"SHOW TABLES\"\n";
echo " {$argv[0]} \"DESCRIBE users\"\n";
echo "\n";
echo "입력 방법:\n";
echo " {$argv[0]} --mysql=production # stdin에서 입력 (Ctrl+D로 완료)\n";
echo " {$argv[0]} < query.sql # 파일에서 입력 (리다이렉션)\n";
echo " cat query.sql | {$argv[0]} # 파일에서 입력 (파이프)\n";
echo " {$argv[0]} \"\$(pbpaste)\" # 클립보드에서 입력 (macOS)\n";
echo " {$argv[0]} \"\$(xclip -o)\" # 클립보드에서 입력 (Linux)\n";
echo "\n";
echo "설정 파일: ~/.ptyMysqlConfig.ini\n";
echo ptyMysqlConfig::getConfigExample() . "\n";
exit(0);
......@@ -75,16 +82,24 @@ if (isset($options['edit'])) {
}
// ============================================================
// 3. 필수 인자 검증
// 3. SQL 입력: 인자 또는 stdin
// ============================================================
if (count($positionalArgs) < 1) {
// stdin에서 읽기
if (posix_isatty(STDIN)) {
fwrite(STDERR, "SQL을 입력하세요 (Ctrl+D로 완료):\n");
}
$sql = file_get_contents('php://stdin');
$sql = trim($sql);
if (empty($sql)) {
echo "Error: SQL 쿼리가 필요합니다.\n";
echo "도움말: {$argv[0]} --help\n";
exit(1);
}
} else {
$sql = $positionalArgs[0];
}
$sql = $positionalArgs[0];
// ANSI 색상 코드
$RED = "\033[1;31m";
$GREEN = "\033[1;32m";
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment