Commit f3fd6732 authored by platyhouse's avatar platyhouse

# Ollama API 지원 추가

## ptyLibrary_PHP/ai/ 라이브러리 확장

### Ollama API 클라이언트 신규 추가
- ptyLibrary_PHP/ai/ollama.api.common.model.php: Ollama API 클라이언트 클래스 신규 작성
  - OpenAI 호환 형식의 /api/chat 엔드포인트 사용
  - setApiUrl(), setModel(), setMaxTokens(), setTemperature() 설정 메서드 제공
  - get(), getSimple(), getWithSystem() API 호출 메서드 구현
  - 대화 기록 유지 기능 (keepHistory 옵션)
  - 디버그 모드 지원 (curl 요청/응답 출력)

### AI 설정 로더 개선
- ptyLibrary_PHP/ai/ptyAIConfig.php:
  - ollama.api.common.model.php 파일 require 추가
  - ollama provider 유효성 검증 추가 (apiKey 필수 검증에서 제외)
  - apiUrl 설정 필드 지원 추가
  - ollama 기본 모델(llama3) 설정 추가
  - createClient()에 OllamaAPIModel 클라이언트 생성 로직 추가
  - getConfigExample()에 ollama 섹션 예시 추가

## ptyAIGetMessage.php CLI 도구 개선

- ptyAIGetMessage.php:
  - 도움말에 ollama 모델 예시 추가 (llama3, mistral, codellama, gemma2)
  - 섹션 목록 출력 시 apiUrl 표시 로직 추가 (ollama용)
  - apiKey 없는 경우 출력 생략 처리
  - extractUsage()에 ollama 토큰 사용량 파싱 추가 (prompt_eval_count, eval_count)

## 문서화

- CLAUDE.md: ptyAIGetMessage 도구 문서 신규 추가
  - CLI 옵션 및 사용 예시
  - ~/.ptyAIConfig.ini 설정 파일 형식
  - 지원 provider 목록 (anthropic, openai, google, ollama)
  - JSON 출력 형식 설명
  - 라이브러리 구조 및 클래스 사용법
  - 각 provider별 토큰 사용량 응답 위치 정리
parent 3bbc8485
......@@ -620,3 +620,171 @@ try {
**필수 옵션:**
- `--mysql=섹션명` - INI 파일 섹션 (기본값: default)
- `--help` - 도움말 출력
## ptyAIGetMessage - AI API CLI 도구
다양한 AI API(Claude, ChatGPT, Gemini, Ollama)를 통해 메시지를 전송하고 응답을 받는 CLI 도구입니다.
### 기본 사용법
```bash
./ptyAIGetMessage.php "메시지" [옵션]
```
### CLI 옵션
| 옵션 | 설명 | 기본값 |
|------|------|--------|
| `--ai=섹션명` | INI 파일 섹션 | `default` |
| `--model=모델명` | 모델 오버라이드 | - |
| `--anthropic-beta=기능명` | Claude 베타 기능 오버라이드 | - |
| `--verbose` | 상세 정보 출력 (토큰 사용량, curl 요청/응답) | - |
| `--json` | JSON 형식으로 출력 | - |
| `--help` | 도움말 출력 | - |
### 사용 예시
```bash
# 기본 사용
./ptyAIGetMessage.php "안녕하세요"
# 특정 섹션 사용
./ptyAIGetMessage.php "안녕" --ai=openai
# 모델 오버라이드
./ptyAIGetMessage.php "안녕" --ai=anthropic --model=claude-3-haiku-20240307
# Claude 베타 기능
./ptyAIGetMessage.php "안녕" --ai=claude --anthropic-beta=context-1m-2025-08-07
# 상세 정보 출력 (curl 요청/응답 포함)
./ptyAIGetMessage.php "안녕" --ai=openai --verbose
# JSON 출력
./ptyAIGetMessage.php "안녕" --ai=google --json
```
### 설정 파일: ~/.ptyAIConfig.ini
```ini
[default]
provider=anthropic
model=claude-sonnet-4-5-20250514
apiKey=your_anthropic_api_key
[claude-beta]
provider=anthropic
model=claude-sonnet-4-5-20250514
apiKey=your_anthropic_api_key
anthropic-beta=context-1m-2025-08-07
[openai]
provider=openai
model=gpt-4o
apiKey=your_openai_api_key
[google]
provider=google
model=gemini-2.0-flash-exp
apiKey=your_google_api_key
[ollama]
provider=ollama
model=llama3
apiUrl=http://localhost:11434
```
### 지원 Provider
| Provider | 설정 필드 | 모델 예시 |
|----------|----------|----------|
| `anthropic` | `apiKey`, `anthropic-beta` | claude-sonnet-4-5-20250514, claude-3-5-sonnet-20241022 |
| `openai` | `apiKey` | gpt-4o, gpt-4-turbo, gpt-3.5-turbo |
| `google` | `apiKey` | gemini-2.0-flash-exp, gemini-1.5-pro |
| `ollama` | `apiUrl` | llama3, mistral, codellama, gemma2 |
### --json 출력 형식
```json
{
"request": {
"provider": "anthropic",
"model": "claude-sonnet-4-5-20250514",
"message": "안녕",
"anthropic-beta": "context-1m-2025-08-07"
},
"response": {
"message": "안녕하세요! 무엇을 도와드릴까요?",
"elapsed_ms": 1234,
"tokens": {
"in": 10,
"out": 25,
"total": 35
},
"_api_result": { ... }
}
}
```
### 라이브러리 구조
```
ptyLibrary_PHP/ai/
├── ptyAIConfig.php # AI 설정 로더
├── claude.api.common.model.php # Anthropic Claude API
├── chatgpt.api.common.model.php # OpenAI ChatGPT API
├── gemini.api.common.model.php # Google Gemini API
└── ollama.api.common.model.php # Ollama API
```
### ptyAIConfig 클래스
```php
// 설정 로드
$config = ptyAIConfig::load('openai');
// 반환: ['provider', 'model', 'apiKey', 'apiUrl', 'anthropic-beta']
// 클라이언트 생성
$client = ptyAIConfig::createClient('openai');
// 설정과 클라이언트 함께 반환
$conn = ptyAIConfig::connect('openai');
$client = $conn['client'];
$config = $conn['config'];
// 간단한 메시지 전송
$response = $client->getSimple("안녕하세요");
```
### 각 API 클라이언트 공통 메서드
```php
$client->setModel($model); // 모델 설정
$client->setMaxTokens($tokens); // 최대 토큰 설정
$client->setTemperature($temp); // Temperature 설정
$client->setDebug(true); // 디버그 모드 (curl 정보 출력)
$client->get($message); // API 호출 (전체 응답)
$client->getSimple($message); // API 호출 (텍스트만)
$client->extractText($response); // 응답에서 텍스트 추출
```
### Claude 전용 메서드
```php
$client->setAnthropicBeta($feature); // anthropic-beta 헤더 설정
```
### Ollama 전용 메서드
```php
$client->setApiUrl($url); // API URL 설정
```
### 토큰 사용량 응답 위치
| Provider | 입력 토큰 | 출력 토큰 |
|----------|----------|----------|
| anthropic | `usage.input_tokens` | `usage.output_tokens` |
| openai | `usage.prompt_tokens` | `usage.completion_tokens` |
| google | `usageMetadata.promptTokenCount` | `usageMetadata.candidatesTokenCount` |
| ollama | `prompt_eval_count` | `eval_count` |
......@@ -46,6 +46,7 @@ if (empty($positionalArgs) || isset($options['help'])) {
fwrite(STDERR, " anthropic: claude-sonnet-4-5-20250514, claude-3-5-sonnet-20241022, claude-3-opus-20240229\n");
fwrite(STDERR, " openai: gpt-4o, gpt-4-turbo, gpt-3.5-turbo\n");
fwrite(STDERR, " google: gemini-2.0-flash-exp, gemini-1.5-pro, gemini-1.5-flash\n");
fwrite(STDERR, " ollama: llama3, mistral, codellama, gemma2\n");
fwrite(STDERR, "\n");
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");
......@@ -61,12 +62,18 @@ if (empty($positionalArgs) || isset($options['help'])) {
foreach ($sections as $name => $config) {
$provider = $config['provider'] ?? '(없음)';
$model = $config['model'] ?? ptyAIConfig::getDefaultModel($provider);
$apiKey = isset($config['apiKey']) ? substr($config['apiKey'], 0, 8) . '...' : '(없음)';
$apiKey = isset($config['apiKey']) ? substr($config['apiKey'], 0, 8) . '...' : null;
$apiUrl = $config['apiUrl'] ?? null;
$anthropicBeta = $config['anthropic-beta'] ?? null;
fwrite(STDERR, "[{$name}]\n");
fwrite(STDERR, " provider: {$provider}\n");
fwrite(STDERR, " model: {$model}\n");
if ($apiKey) {
fwrite(STDERR, " apiKey: {$apiKey}\n");
}
if ($apiUrl) {
fwrite(STDERR, " apiUrl: {$apiUrl}\n");
}
if ($anthropicBeta) {
fwrite(STDERR, " anthropic-beta: {$anthropicBeta}\n");
}
......@@ -242,6 +249,16 @@ function extractUsage($response, $provider)
];
}
break;
case 'ollama':
// Ollama: prompt_eval_count, eval_count
$input = $response['prompt_eval_count'] ?? 0;
$output = $response['eval_count'] ?? 0;
return [
'input' => $input,
'output' => $output,
'total' => $input + $output,
];
}
return null;
......
<?php
namespace platyFramework;
require_once(__DIR__ . "/../ptycommon/model.php");
/**
* Ollama API Model
* Ollama API를 사용하기 위한 클래스 (OpenAI 호환 API)
*/
class OllamaAPIModel extends model
{
private $apiUrl = 'http://localhost:11434';
private $model = 'llama3';
private $maxTokens = 4096;
private $temperature = 1.0;
private $conversationHistory = [];
private $debug = false;
/**
* API URL 설정
* @param string $apiUrl Ollama 서버 URL
* @return $this
*/
public function setApiUrl($apiUrl)
{
$this->apiUrl = rtrim($apiUrl, '/');
return $this;
}
/**
* 모델 설정
* @param string $model 사용할 모델명 (llama3, mistral, codellama 등)
* @return $this
*/
public function setModel($model)
{
$this->model = $model;
return $this;
}
/**
* 최대 토큰 수 설정
* @param int $maxTokens
* @return $this
*/
public function setMaxTokens($maxTokens)
{
$this->maxTokens = $maxTokens;
return $this;
}
/**
* Temperature 설정
* @param float $temperature 0.0 ~ 2.0
* @return $this
*/
public function setTemperature($temperature)
{
$this->temperature = $temperature;
return $this;
}
/**
* 대화 기록 초기화
* @return $this
*/
public function resetConversation()
{
$this->conversationHistory = [];
return $this;
}
/**
* 디버그 모드 설정
* @param bool $debug 디버그 모드 활성화 여부
* @return $this
*/
public function setDebug($debug)
{
$this->debug = $debug;
return $this;
}
/**
* Ollama API 호출
* @param string $message 사용자 메시지
* @param bool $keepHistory 대화 기록 유지 여부
* @return array|false 응답 배열 또는 false
*/
public function get($message, $keepHistory = false)
{
// 현재 메시지를 대화 기록에 추가
$messages = $this->conversationHistory;
$messages[] = [
'role' => 'user',
'content' => $message
];
// API 요청 데이터 구성 (OpenAI 호환 형식)
$data = [
'model' => $this->model,
'messages' => $messages,
'options' => [
'num_predict' => $this->maxTokens,
'temperature' => $this->temperature,
],
'stream' => false,
];
// API 호출
$response = $this->_sendRequest($data);
if ($response === false) {
return false;
}
// 대화 기록 유지
if ($keepHistory && isset($response['message']['content'])) {
$this->conversationHistory = $messages;
$this->conversationHistory[] = [
'role' => 'assistant',
'content' => $response['message']['content']
];
}
return $response;
}
/**
* 시스템 프롬프트와 함께 메시지 전송
* @param string $message 사용자 메시지
* @param string $systemPrompt 시스템 프롬프트
* @return array|false
*/
public function getWithSystem($message, $systemPrompt)
{
$messages = [
[
'role' => 'system',
'content' => $systemPrompt
],
[
'role' => 'user',
'content' => $message
]
];
$data = [
'model' => $this->model,
'messages' => $messages,
'options' => [
'num_predict' => $this->maxTokens,
'temperature' => $this->temperature,
],
'stream' => false,
];
return $this->_sendRequest($data);
}
/**
* API 요청 전송
* @param array $data 요청 데이터
* @return array|false
*/
private function _sendRequest($data)
{
// API 호출 타임아웃 방지
set_time_limit(0);
ini_set('max_execution_time', '0');
$url = $this->apiUrl . '/api/chat';
$ch = curl_init($url);
$headers = [
'Content-Type: application/json'
];
$jsonData = json_encode($data);
// 디버그 출력
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");
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 300); // 5분 타임아웃
$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) {
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 API cURL Error: " . $error);
return false;
}
if ($httpCode !== 200) {
error_log("Ollama API HTTP Error: " . $httpCode . " - " . $response);
return false;
}
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("Ollama API JSON Decode Error: " . json_last_error_msg());
return false;
}
return $result;
}
/**
* 응답에서 텍스트만 추출
* @param array $response API 응답
* @return string|false
*/
public function extractText($response)
{
if (isset($response['message']['content'])) {
return $response['message']['content'];
}
return false;
}
/**
* 간단한 텍스트 응답만 받기
* @param string $message
* @return string|false
*/
public function getSimple($message)
{
$response = $this->get($message);
return $this->extractText($response);
}
}
......@@ -16,6 +16,7 @@ namespace platyFramework;
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";
/**
* AI 설정 로더 클래스
......@@ -78,16 +79,20 @@ class ptyAIConfig
$sectionConfig = $config[$section];
// 필수 필드 검증
$requiredFields = ['provider', 'apiKey'];
foreach ($requiredFields as $field) {
if (!isset($sectionConfig[$field]) || empty($sectionConfig[$field])) {
throw new \Exception("AI 설정 [{$section}] 섹션에 필수 필드 '{$field}'가 없습니다.");
}
// provider 먼저 확인
if (!isset($sectionConfig['provider']) || empty($sectionConfig['provider'])) {
throw new \Exception("AI 설정 [{$section}] 섹션에 필수 필드 'provider'가 없습니다.");
}
$provider = strtolower($sectionConfig['provider']);
$validProviders = ['anthropic', 'openai', 'google'];
// apiKey 필수 검증 (ollama 제외)
if ($provider !== 'ollama') {
if (!isset($sectionConfig['apiKey']) || empty($sectionConfig['apiKey'])) {
throw new \Exception("AI 설정 [{$section}] 섹션에 필수 필드 'apiKey'가 없습니다.");
}
}
$validProviders = ['anthropic', 'openai', 'google', 'ollama'];
if (!in_array($provider, $validProviders)) {
throw new \Exception("AI 설정 [{$section}] 섹션의 provider '{$provider}'는 지원되지 않습니다.\n지원되는 provider: " . implode(', ', $validProviders));
}
......@@ -101,7 +106,8 @@ class ptyAIConfig
return [
'provider' => $provider,
'model' => $sectionConfig['model'] ?? self::getDefaultModel($provider),
'apiKey' => trim($sectionConfig['apiKey'], '"\''),
'apiKey' => isset($sectionConfig['apiKey']) ? trim($sectionConfig['apiKey'], '"\'') : null,
'apiUrl' => isset($sectionConfig['apiUrl']) ? trim($sectionConfig['apiUrl'], '"\'') : null,
'anthropic-beta' => $anthropicBeta,
];
}
......@@ -115,6 +121,7 @@ class ptyAIConfig
'anthropic' => 'claude-3-5-sonnet-20241022',
'openai' => 'gpt-4o',
'google' => 'gemini-2.0-flash-exp',
'ollama' => 'llama3',
];
return $defaults[$provider] ?? null;
}
......@@ -150,6 +157,14 @@ class ptyAIConfig
$client->setModel($config['model']);
return $client;
case 'ollama':
$client = new OllamaAPIModel();
$client->setModel($config['model']);
if (!empty($config['apiUrl'])) {
$client->setApiUrl($config['apiUrl']);
}
return $client;
default:
throw new \Exception("지원되지 않는 provider: {$config['provider']}");
}
......@@ -201,8 +216,14 @@ provider=google
model=gemini-2.0-flash-exp
apiKey=your_google_api_key
# 지원 provider: anthropic, openai, google
[ollama]
provider=ollama
model=llama3
apiUrl=http://localhost:11434
# 지원 provider: anthropic, openai, google, ollama
# anthropic-beta: Claude 베타 기능 사용 시 헤더값 (예: context-1m-2025-08-07)
# apiUrl: ollama 서버 URL (기본값: http://localhost:11434)
EOT;
}
}
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