Coverage for src/refinire/agents/pipeline/llm_pipeline.py: 76%
379 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-15 18:51 +0900
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-15 18:51 +0900
1"""LLM Pipeline - A replacement for deprecated AgentPipeline using OpenAI Python SDK directly.
3LLMパイプライン - 非推奨のAgentPipelineに代わって、OpenAI Python SDKを直接使用する新しい実装。
4"""
6from __future__ import annotations
8import asyncio
9import json
10from typing import Any, Callable, Dict, List, Optional, Type, Union
11from dataclasses import dataclass, field
12from concurrent.futures import ThreadPoolExecutor
13import warnings
15try:
16 from openai import OpenAI, AsyncOpenAI
17 from pydantic import BaseModel
18except ImportError as e:
19 raise ImportError(f"Required dependencies not found: {e}. Please install openai and pydantic.")
21try:
22 from ...core.prompt_store import PromptReference
23except ImportError:
24 PromptReference = None
27@dataclass
28class LLMResult:
29 """
30 Result from LLM generation
31 LLM生成結果
33 Attributes:
34 content: Generated content / 生成されたコンテンツ
35 success: Whether generation was successful / 生成が成功したか
36 metadata: Additional metadata / 追加メタデータ
37 evaluation_score: Evaluation score if evaluated / 評価されている場合の評価スコア
38 attempts: Number of attempts made / 実行された試行回数
39 """
40 content: Any
41 success: bool = True
42 metadata: Dict[str, Any] = field(default_factory=dict)
43 evaluation_score: Optional[float] = None
44 attempts: int = 1
47@dataclass
48class EvaluationResult:
49 """
50 Result from evaluation process
51 評価プロセスの結果
53 Attributes:
54 score: Evaluation score (0-100) / 評価スコア(0-100)
55 passed: Whether evaluation passed threshold / 閾値を超えたか
56 feedback: Evaluation feedback / 評価フィードバック
57 metadata: Additional metadata / 追加メタデータ
58 """
59 score: float
60 passed: bool
61 feedback: Optional[str] = None
62 metadata: Dict[str, Any] = field(default_factory=dict)
65class LLMPipeline:
66 """
67 Modern LLM Pipeline using OpenAI Python SDK directly
68 OpenAI Python SDKを直接使用するモダンなLLMパイプライン
70 This class replaces the deprecated AgentPipeline with a cleaner, more maintainable implementation.
71 このクラスは非推奨のAgentPipelineを、よりクリーンで保守しやすい実装で置き換えます。
72 """
74 def __init__(
75 self,
76 name: str,
77 generation_instructions: str,
78 evaluation_instructions: Optional[str] = None,
79 *,
80 model: str = "gpt-4o-mini",
81 evaluation_model: Optional[str] = None,
82 output_model: Optional[Type[BaseModel]] = None,
83 temperature: float = 0.7,
84 max_tokens: Optional[int] = None,
85 timeout: float = 30.0,
86 threshold: float = 85.0,
87 max_retries: int = 3,
88 input_guardrails: Optional[List[Callable[[str], bool]]] = None,
89 output_guardrails: Optional[List[Callable[[Any], bool]]] = None,
90 session_history: Optional[List[str]] = None,
91 history_size: int = 10,
92 improvement_callback: Optional[Callable[[LLMResult, EvaluationResult], str]] = None,
93 locale: str = "en",
94 tools: Optional[List[Dict]] = None,
95 mcp_servers: Optional[List[str]] = None
96 ) -> None:
97 """
98 Initialize LLM Pipeline
99 LLMパイプラインを初期化する
101 Args:
102 name: Pipeline name / パイプライン名
103 generation_instructions: Instructions for generation / 生成用指示
104 evaluation_instructions: Instructions for evaluation / 評価用指示
105 model: OpenAI model name / OpenAIモデル名
106 evaluation_model: Model for evaluation / 評価用モデル
107 output_model: Pydantic model for structured output / 構造化出力用Pydanticモデル
108 temperature: Sampling temperature / サンプリング温度
109 max_tokens: Maximum tokens / 最大トークン数
110 timeout: Request timeout / リクエストタイムアウト
111 threshold: Evaluation threshold / 評価閾値
112 max_retries: Maximum retry attempts / 最大リトライ回数
113 input_guardrails: Input validation functions / 入力検証関数
114 output_guardrails: Output validation functions / 出力検証関数
115 session_history: Session history / セッション履歴
116 history_size: History size limit / 履歴サイズ制限
117 improvement_callback: Callback for improvement suggestions / 改善提案コールバック
118 locale: Locale for messages / メッセージ用ロケール
119 tools: OpenAI function tools / OpenAI関数ツール
120 mcp_servers: MCP server identifiers / MCPサーバー識別子
121 """
122 # Basic configuration
123 self.name = name
125 # Handle PromptReference for generation instructions
126 self._generation_prompt_metadata = None
127 if PromptReference and isinstance(generation_instructions, PromptReference):
128 self._generation_prompt_metadata = generation_instructions.get_metadata()
129 self.generation_instructions = str(generation_instructions)
130 else:
131 self.generation_instructions = generation_instructions
133 # Handle PromptReference for evaluation instructions
134 self._evaluation_prompt_metadata = None
135 if PromptReference and isinstance(evaluation_instructions, PromptReference):
136 self._evaluation_prompt_metadata = evaluation_instructions.get_metadata()
137 self.evaluation_instructions = str(evaluation_instructions)
138 else:
139 self.evaluation_instructions = evaluation_instructions
141 self.model = model
142 self.evaluation_model = evaluation_model or model
143 self.output_model = output_model
144 self.temperature = temperature
145 self.max_tokens = max_tokens
146 self.timeout = timeout
147 self.threshold = threshold
148 self.max_retries = max_retries
149 self.locale = locale
151 # Guardrails
152 self.input_guardrails = input_guardrails or []
153 self.output_guardrails = output_guardrails or []
155 # History management
156 self.session_history = session_history or []
157 self.history_size = history_size
158 self._pipeline_history: List[Dict[str, Any]] = []
160 # Callbacks
161 self.improvement_callback = improvement_callback
163 # Tools and MCP support
164 self.tools = tools or []
165 self.mcp_servers = mcp_servers or []
167 # Initialize OpenAI clients
168 self.sync_client = OpenAI()
169 self.async_client = AsyncOpenAI()
171 def run(self, user_input: str) -> LLMResult:
172 """
173 Run the pipeline synchronously
174 パイプラインを同期的に実行する
176 Args:
177 user_input: User input / ユーザー入力
179 Returns:
180 LLMResult: Generation result / 生成結果
181 """
182 # Input validation
183 if not self._validate_input(user_input):
184 return LLMResult(
185 content=None,
186 success=False,
187 metadata={"error": "Input validation failed", "input": user_input}
188 )
190 # Build prompt with history
191 full_prompt = self._build_prompt(user_input)
193 # Generation with retries
194 for attempt in range(1, self.max_retries + 1):
195 try:
196 # Generate content
197 generation_result = self._generate_content(full_prompt)
199 # Output validation
200 if not self._validate_output(generation_result):
201 if attempt < self.max_retries:
202 continue
203 return LLMResult(
204 content=None,
205 success=False,
206 metadata={"error": "Output validation failed", "attempts": attempt}
207 )
209 # Parse structured output if model specified
210 parsed_content = self._parse_structured_output(generation_result)
212 # Evaluate if evaluation instructions provided
213 evaluation_result = None
214 if self.evaluation_instructions:
215 evaluation_result = self._evaluate_content(user_input, parsed_content)
217 # Check if evaluation passed
218 if not evaluation_result.passed and attempt < self.max_retries:
219 # Generate improvement if callback provided
220 if self.improvement_callback:
221 improvement = self.improvement_callback(
222 LLMResult(content=parsed_content, success=True),
223 evaluation_result
224 )
225 full_prompt = f"{full_prompt}\n\nImprovement needed: {improvement}"
226 continue
228 # Success - store in history and return
229 metadata = {
230 "model": self.model,
231 "temperature": self.temperature,
232 "attempts": attempt
233 }
235 # Add prompt metadata if available
236 if self._generation_prompt_metadata:
237 metadata.update(self._generation_prompt_metadata)
239 if self._evaluation_prompt_metadata:
240 metadata["evaluation_prompt"] = self._evaluation_prompt_metadata
242 result = LLMResult(
243 content=parsed_content,
244 success=True,
245 metadata=metadata,
246 evaluation_score=evaluation_result.score if evaluation_result else None,
247 attempts=attempt
248 )
250 self._store_in_history(user_input, result)
251 return result
253 except Exception as e:
254 if attempt == self.max_retries:
255 return LLMResult(
256 content=None,
257 success=False,
258 metadata={"error": str(e), "attempts": attempt}
259 )
260 continue
262 # Should not reach here
263 return LLMResult(
264 content=None,
265 success=False,
266 metadata={"error": "Maximum retries exceeded"}
267 )
269 async def run_async(self, user_input: str) -> LLMResult:
270 """
271 Run the pipeline asynchronously
272 パイプラインを非同期的に実行する
274 Args:
275 user_input: User input / ユーザー入力
277 Returns:
278 LLMResult: Generation result / 生成結果
279 """
280 # For simplicity, run sync version in executor
281 # In production, this would be fully async
282 loop = asyncio.get_event_loop()
283 with ThreadPoolExecutor() as executor:
284 return await loop.run_in_executor(executor, self.run, user_input)
286 def _validate_input(self, user_input: str) -> bool:
287 """Validate input using guardrails / ガードレールを使用して入力を検証"""
288 for guardrail in self.input_guardrails:
289 if not guardrail(user_input):
290 return False
291 return True
293 def _validate_output(self, output: str) -> bool:
294 """Validate output using guardrails / ガードレールを使用して出力を検証"""
295 for guardrail in self.output_guardrails:
296 if not guardrail(output):
297 return False
298 return True
300 def _build_prompt(self, user_input: str) -> str:
301 """Build complete prompt with instructions and history / 指示と履歴を含む完全なプロンプトを構築"""
302 prompt_parts = [self.generation_instructions]
304 # Add history if available
305 if self.session_history:
306 history_text = "\n".join(self.session_history[-self.history_size:])
307 prompt_parts.append(f"Previous context:\n{history_text}")
309 prompt_parts.append(f"User input: {user_input}")
311 return "\n\n".join(prompt_parts)
313 def _generate_content(self, prompt: str) -> str:
314 """Generate content using OpenAI API with full tool support / OpenAI APIを使用した完全なtoolサポート付きコンテンツ生成"""
315 messages = [{"role": "user", "content": prompt}]
317 # Tools/function calling loop
318 # Tools/function calling ループ
319 max_tool_iterations = 10 # Prevent infinite loops / 無限ループを防ぐ
320 iteration = 0
322 while iteration < max_tool_iterations:
323 # Prepare API call parameters
324 # API呼び出しパラメータを準備
325 params = {
326 "model": self.model,
327 "messages": messages,
328 "temperature": self.temperature,
329 "timeout": self.timeout
330 }
332 if self.max_tokens:
333 params["max_tokens"] = self.max_tokens
335 # Add tools if available
336 # toolsが利用可能な場合は追加
337 if self.tools:
338 params["tools"] = self.tools
339 params["tool_choice"] = "auto"
341 # Add structured output if model specified
342 # 構造化出力が指定されている場合は追加
343 if self.output_model:
344 params["response_format"] = {"type": "json_object"}
346 response = self.sync_client.chat.completions.create(**params)
347 message = response.choices[0].message
349 # Add assistant message to conversation
350 # アシスタントメッセージを対話に追加
351 messages.append({
352 "role": "assistant",
353 "content": message.content,
354 "tool_calls": [
355 {
356 "id": tc.id,
357 "type": tc.type,
358 "function": {
359 "name": tc.function.name,
360 "arguments": tc.function.arguments
361 }
362 }
363 for tc in (message.tool_calls or [])
364 ]
365 })
367 # If no tool calls, we're done
368 # tool呼び出しがない場合は完了
369 if not message.tool_calls:
370 return message.content or ""
372 # Execute tool calls
373 # tool呼び出しを実行
374 for tool_call in message.tool_calls:
375 try:
376 # Execute the tool function
377 # tool関数を実行
378 tool_result = self._execute_tool(tool_call)
380 # Add tool result to conversation
381 # tool結果を対話に追加
382 messages.append({
383 "role": "tool",
384 "tool_call_id": tool_call.id,
385 "content": str(tool_result)
386 })
388 except Exception as e:
389 # Handle tool execution errors
390 # tool実行エラーを処理
391 messages.append({
392 "role": "tool",
393 "tool_call_id": tool_call.id,
394 "content": f"Error executing tool: {str(e)}"
395 })
397 iteration += 1
399 # If we reach here, we hit the iteration limit
400 # ここに到達した場合は反復制限に達した
401 return "Maximum tool iteration limit reached. Please try a simpler request."
403 def _execute_tool(self, tool_call) -> Any:
404 """Execute a tool function call / tool関数呼び出しを実行"""
405 function_name = tool_call.function.name
406 arguments = json.loads(tool_call.function.arguments)
408 # Find the tool by name
409 # 名前でtoolを検索
410 for tool in self.tools:
411 if tool.get("function", {}).get("name") == function_name:
412 # Check if tool has a callable function
413 # toolに呼び出し可能な関数があるかチェック
414 if "callable" in tool:
415 return tool["callable"](**arguments)
416 elif "handler" in tool:
417 return tool["handler"](**arguments)
418 else:
419 # Try to find a Python function with the same name
420 # 同じ名前のPython関数を検索
421 import inspect
422 frame = inspect.currentframe()
423 while frame:
424 if function_name in frame.f_globals:
425 func = frame.f_globals[function_name]
426 if callable(func):
427 return func(**arguments)
428 frame = frame.f_back
430 raise ValueError(f"No callable implementation found for tool: {function_name}")
432 raise ValueError(f"Tool not found: {function_name}")
434 def add_tool(self, tool_definition: Dict, handler: Optional[callable] = None) -> None:
435 """Add a tool to the pipeline / パイプラインにtoolを追加"""
436 if handler:
437 tool_definition["callable"] = handler
438 self.tools.append(tool_definition)
440 def add_function_tool(
441 self,
442 func: callable,
443 name: Optional[str] = None,
444 description: Optional[str] = None
445 ) -> None:
446 """Add a Python function as a tool / Python関数をtoolとして追加"""
447 import inspect
449 function_name = name or func.__name__
450 function_description = description or func.__doc__ or f"Function: {function_name}"
452 # Get function signature for parameters
453 # パラメータ用の関数シグネチャを取得
454 sig = inspect.signature(func)
455 parameters = {}
456 required = []
458 for param_name, param in sig.parameters.items():
459 param_info = {"type": "string"} # Default type
461 # Try to infer type from annotation
462 # アノテーションから型を推論
463 if param.annotation != inspect.Parameter.empty:
464 if param.annotation == int:
465 param_info["type"] = "integer"
466 elif param.annotation == float:
467 param_info["type"] = "number"
468 elif param.annotation == bool:
469 param_info["type"] = "boolean"
470 elif param.annotation == list:
471 param_info["type"] = "array"
472 elif param.annotation == dict:
473 param_info["type"] = "object"
475 parameters[param_name] = param_info
477 # Add to required if no default value
478 # デフォルト値がない場合は必須に追加
479 if param.default == inspect.Parameter.empty:
480 required.append(param_name)
482 tool_definition = {
483 "type": "function",
484 "function": {
485 "name": function_name,
486 "description": function_description,
487 "parameters": {
488 "type": "object",
489 "properties": parameters,
490 "required": required
491 }
492 },
493 "callable": func
494 }
496 self.tools.append(tool_definition)
498 def add_mcp_server(self, server_config: Dict) -> None:
499 """Add MCP server configuration / MCPサーバー設定を追加"""
500 # MCP server integration would be implemented here
501 # MCPサーバー統合をここで実装
502 self.mcp_servers.append(server_config)
504 # For now, log that MCP is configured but not fully implemented
505 # 現在のところ、MCPが設定されているがまだ完全に実装されていないことをログ
506 print(f"MCP server configured: {server_config.get('name', 'unnamed')}")
507 print("Note: Full MCP integration is planned for future release")
509 def remove_tool(self, tool_name: str) -> bool:
510 """Remove a tool by name / 名前でtoolを削除"""
511 for i, tool in enumerate(self.tools):
512 if tool.get("function", {}).get("name") == tool_name:
513 del self.tools[i]
514 return True
515 return False
517 def list_tools(self) -> List[str]:
518 """List all available tool names / 利用可能なtool名をリスト"""
519 return [
520 tool.get("function", {}).get("name", "unnamed")
521 for tool in self.tools
522 ]
524 def _parse_structured_output(self, content: str) -> Any:
525 """Parse structured output if model specified / モデルが指定されている場合は構造化出力を解析"""
526 if not self.output_model:
527 return content
529 try:
530 # Parse JSON and validate with Pydantic model
531 data = json.loads(content)
532 return self.output_model.model_validate(data)
533 except Exception:
534 # Fallback to raw content if parsing fails
535 return content
537 def _evaluate_content(self, user_input: str, generated_content: Any) -> EvaluationResult:
538 """Evaluate generated content / 生成されたコンテンツを評価"""
539 evaluation_prompt = f"""
540{self.evaluation_instructions}
542User Input: {user_input}
543Generated Content: {generated_content}
545Please provide a score from 0 to 100 and brief feedback.
546Return your response as JSON with 'score' and 'feedback' fields.
547"""
549 messages = [{"role": "user", "content": evaluation_prompt}]
551 try:
552 response = self.sync_client.chat.completions.create(
553 model=self.evaluation_model,
554 messages=messages,
555 temperature=0.3, # Lower temperature for evaluation
556 response_format={"type": "json_object"},
557 timeout=self.timeout
558 )
560 eval_data = json.loads(response.choices[0].message.content)
561 score = float(eval_data.get("score", 0))
562 feedback = eval_data.get("feedback", "")
564 return EvaluationResult(
565 score=score,
566 passed=score >= self.threshold,
567 feedback=feedback,
568 metadata={"model": self.evaluation_model}
569 )
571 except Exception as e:
572 # Fallback evaluation
573 return EvaluationResult(
574 score=0.0,
575 passed=False,
576 feedback=f"Evaluation failed: {str(e)}",
577 metadata={"error": str(e)}
578 )
580 def _store_in_history(self, user_input: str, result: LLMResult) -> None:
581 """Store interaction in history / 対話を履歴に保存"""
582 interaction = {
583 "user_input": user_input,
584 "result": result.content,
585 "success": result.success,
586 "metadata": result.metadata,
587 "timestamp": json.dumps({"pipeline": self.name}, ensure_ascii=False)
588 }
590 self._pipeline_history.append(interaction)
592 # Add to session history for context
593 session_entry = f"User: {user_input}\nAssistant: {result.content}"
594 self.session_history.append(session_entry)
596 # Trim history if needed
597 if len(self.session_history) > self.history_size:
598 self.session_history = self.session_history[-self.history_size:]
600 def clear_history(self) -> None:
601 """Clear all history / 全履歴をクリア"""
602 self._pipeline_history.clear()
603 self.session_history.clear()
605 def get_history(self) -> List[Dict[str, Any]]:
606 """Get pipeline history / パイプライン履歴を取得"""
607 return self._pipeline_history.copy()
609 def update_instructions(
610 self,
611 generation_instructions: Optional[str] = None,
612 evaluation_instructions: Optional[str] = None
613 ) -> None:
614 """Update instructions / 指示を更新"""
615 if generation_instructions:
616 self.generation_instructions = generation_instructions
617 if evaluation_instructions:
618 self.evaluation_instructions = evaluation_instructions
620 def set_threshold(self, threshold: float) -> None:
621 """Set evaluation threshold / 評価閾値を設定"""
622 if 0 <= threshold <= 100:
623 self.threshold = threshold
624 else:
625 raise ValueError("Threshold must be between 0 and 100")
627 def __str__(self) -> str:
628 return f"LLMPipeline(name={self.name}, model={self.model})"
630 def __repr__(self) -> str:
631 return self.__str__()
634# Utility functions for common configurations
635# 共通設定用のユーティリティ関数
637def create_simple_llm_pipeline(
638 name: str,
639 instructions: str,
640 model: str = "gpt-4o-mini",
641 **kwargs
642) -> LLMPipeline:
643 """
644 Create a simple LLM pipeline
645 シンプルなLLMパイプラインを作成
646 """
647 return LLMPipeline(
648 name=name,
649 generation_instructions=instructions,
650 model=model,
651 **kwargs
652 )
655def create_evaluated_llm_pipeline(
656 name: str,
657 generation_instructions: str,
658 evaluation_instructions: str,
659 model: str = "gpt-4o-mini",
660 evaluation_model: Optional[str] = None,
661 threshold: float = 85.0,
662 **kwargs
663) -> LLMPipeline:
664 """
665 Create an LLM pipeline with evaluation
666 評価機能付きLLMパイプラインを作成
667 """
668 return LLMPipeline(
669 name=name,
670 generation_instructions=generation_instructions,
671 evaluation_instructions=evaluation_instructions,
672 model=model,
673 evaluation_model=evaluation_model,
674 threshold=threshold,
675 **kwargs
676 )
679def create_tool_enabled_llm_pipeline(
680 name: str,
681 instructions: str,
682 tools: Optional[List[callable]] = None,
683 model: str = "gpt-4o-mini",
684 **kwargs
685) -> LLMPipeline:
686 """
687 Create an LLM pipeline with automatic tool registration
688 自動tool登録機能付きLLMパイプラインを作成
690 Args:
691 name: Pipeline name / パイプライン名
692 instructions: System instructions / システム指示
693 tools: List of Python functions to register as tools / tool登録するPython関数のリスト
694 model: LLM model name / LLMモデル名
695 **kwargs: Additional arguments for LLMPipeline / LLMPipeline用追加引数
697 Returns:
698 LLMPipeline: Configured pipeline with tools / tool設定済みパイプライン
700 Example:
701 >>> def get_weather(city: str) -> str:
702 ... '''Get weather for a city'''
703 ... return f"Weather in {city}: Sunny"
704 ...
705 >>> def calculate(expression: str) -> float:
706 ... '''Calculate mathematical expression'''
707 ... return eval(expression)
708 ...
709 >>> pipeline = create_tool_enabled_llm_pipeline(
710 ... name="assistant",
711 ... instructions="You are a helpful assistant with access to tools.",
712 ... tools=[get_weather, calculate]
713 ... )
714 >>> result = pipeline.run("What's the weather in Tokyo and what's 2+2?")
715 """
716 pipeline = LLMPipeline(
717 name=name,
718 generation_instructions=instructions,
719 model=model,
720 tools=[], # Start with empty tools list
721 **kwargs
722 )
724 # Register all provided tools
725 # 提供されたすべてのtoolを登録
726 if tools:
727 for tool_func in tools:
728 pipeline.add_function_tool(tool_func)
730 return pipeline
733def create_web_search_pipeline(
734 name: str,
735 instructions: str = "You are a helpful assistant with access to web search. Use web search when you need current information.",
736 model: str = "gpt-4o-mini",
737 **kwargs
738) -> LLMPipeline:
739 """
740 Create an LLM pipeline with web search capability
741 Web検索機能付きLLMパイプラインを作成
743 Note: This is a template - actual web search implementation would require
744 integration with search APIs like Google Search API, Bing API, etc.
745 注意:これはテンプレートです。実際のWeb検索実装には
746 Google Search API、Bing APIなどとの統合が必要です。
747 """
748 def web_search(query: str) -> str:
749 """Search the web for information (placeholder implementation)"""
750 # This is a placeholder implementation
751 # Real implementation would use actual search APIs
752 return f"Web search results for '{query}': [This is a placeholder. Integrate with actual search API.]"
754 return create_tool_enabled_llm_pipeline(
755 name=name,
756 instructions=instructions,
757 tools=[web_search],
758 model=model,
759 **kwargs
760 )
763def create_calculator_pipeline(
764 name: str,
765 instructions: str = "You are a helpful assistant with calculation capabilities. Use the calculator for mathematical computations.",
766 model: str = "gpt-4o-mini",
767 **kwargs
768) -> LLMPipeline:
769 """
770 Create an LLM pipeline with calculation capability
771 計算機能付きLLMパイプラインを作成
772 """
773 def calculate(expression: str) -> float:
774 """Calculate mathematical expression safely"""
775 try:
776 # For production, use a safer expression evaluator
777 # 本番環境では、より安全な式評価器を使用
778 import ast
779 import operator
781 # Allowed operations
782 operators = {
783 ast.Add: operator.add,
784 ast.Sub: operator.sub,
785 ast.Mult: operator.mul,
786 ast.Div: operator.truediv,
787 ast.Pow: operator.pow,
788 ast.Mod: operator.mod,
789 ast.USub: operator.neg,
790 }
792 def eval_expr(expr):
793 if isinstance(expr, ast.Num):
794 return expr.n
795 elif isinstance(expr, ast.Constant):
796 return expr.value
797 elif isinstance(expr, ast.BinOp):
798 return operators[type(expr.op)](eval_expr(expr.left), eval_expr(expr.right))
799 elif isinstance(expr, ast.UnaryOp):
800 return operators[type(expr.op)](eval_expr(expr.operand))
801 else:
802 raise TypeError(f"Unsupported operation: {type(expr)}")
804 tree = ast.parse(expression, mode='eval')
805 return eval_expr(tree.body)
807 except Exception as e:
808 return f"Error calculating '{expression}': {str(e)}"
810 return create_tool_enabled_llm_pipeline(
811 name=name,
812 instructions=instructions,
813 tools=[calculate],
814 model=model,
815 **kwargs
816 )
819@dataclass
820class InteractionQuestion:
821 """
822 Represents a question from the interactive pipeline
823 対話的パイプラインからの質問を表現するクラス
825 Attributes:
826 question: The question text / 質問テキスト
827 turn: Current turn number / 現在のターン番号
828 remaining_turns: Remaining turns / 残りターン数
829 metadata: Additional metadata / 追加メタデータ
830 """
831 question: str # The question text / 質問テキスト
832 turn: int # Current turn number / 現在のターン番号
833 remaining_turns: int # Remaining turns / 残りターン数
834 metadata: Dict[str, Any] = field(default_factory=dict) # Additional metadata / 追加メタデータ
836 def __str__(self) -> str:
837 """
838 String representation of the interaction question
839 対話質問の文字列表現
841 Returns:
842 str: Formatted question with turn info / ターン情報付きフォーマット済み質問
843 """
844 return f"[Turn {self.turn}/{self.turn + self.remaining_turns}] {self.question}"
847@dataclass
848class InteractionResult:
849 """
850 Result from interactive pipeline execution
851 対話的パイプライン実行の結果
853 Attributes:
854 is_complete: True if interaction is complete / 対話が完了した場合True
855 content: Result content or next question / 結果コンテンツまたは次の質問
856 turn: Current turn number / 現在のターン番号
857 remaining_turns: Remaining turns / 残りターン数
858 success: Whether execution was successful / 実行が成功したか
859 metadata: Additional metadata / 追加メタデータ
860 """
861 is_complete: bool # True if interaction is complete / 対話が完了した場合True
862 content: Any # Result content or next question / 結果コンテンツまたは次の質問
863 turn: int # Current turn number / 現在のターン番号
864 remaining_turns: int # Remaining turns / 残りターン数
865 success: bool = True # Whether execution was successful / 実行が成功したか
866 metadata: Dict[str, Any] = field(default_factory=dict) # Additional metadata / 追加メタデータ
869class InteractivePipeline(LLMPipeline):
870 """
871 Interactive Pipeline for multi-turn conversations using LLMPipeline
872 LLMPipelineを使用した複数ターン会話のための対話的パイプライン
874 This class extends LLMPipeline to handle:
875 このクラスはLLMPipelineを拡張して以下を処理します:
876 - Multi-turn interactive conversations / 複数ターンの対話的会話
877 - Completion condition checking / 完了条件のチェック
878 - Turn management / ターン管理
879 - Conversation history tracking / 会話履歴の追跡
881 The pipeline uses a completion check function to determine when the interaction is finished.
882 パイプラインは完了チェック関数を使用して対話の終了時期を判定します。
883 """
885 def __init__(
886 self,
887 name: str,
888 generation_instructions: str,
889 completion_check: Callable[[Any], bool],
890 max_turns: int = 20,
891 evaluation_instructions: Optional[str] = None,
892 question_format: Optional[Callable[[str, int, int], str]] = None,
893 **kwargs
894 ) -> None:
895 """
896 Initialize the InteractivePipeline
897 InteractivePipelineを初期化する
899 Args:
900 name: Pipeline name / パイプライン名
901 generation_instructions: System prompt for generation / 生成用システムプロンプト
902 completion_check: Function to check if interaction is complete / 対話完了チェック関数
903 max_turns: Maximum number of interaction turns / 最大対話ターン数
904 evaluation_instructions: System prompt for evaluation / 評価用システムプロンプト
905 question_format: Optional function to format questions / 質問フォーマット関数(任意)
906 **kwargs: Additional arguments for LLMPipeline / LLMPipeline用追加引数
907 """
908 # Initialize base LLMPipeline
909 # ベースのLLMPipelineを初期化
910 super().__init__(
911 name=name,
912 generation_instructions=generation_instructions,
913 evaluation_instructions=evaluation_instructions,
914 **kwargs
915 )
917 # Interactive-specific configuration
918 # 対話固有の設定
919 self.completion_check = completion_check
920 self.max_turns = max_turns
921 self.question_format = question_format or self._default_question_format
923 # Interaction state
924 # 対話状態
925 self._turn_count = 0
926 self._conversation_history: List[Dict[str, Any]] = []
927 self._is_complete = False
928 self._final_result: Any = None
930 def run_interactive(self, initial_input: str) -> InteractionResult:
931 """
932 Start an interactive conversation
933 対話的会話を開始する
935 Args:
936 initial_input: Initial user input / 初期ユーザー入力
938 Returns:
939 InteractionResult: Initial interaction result / 初期対話結果
940 """
941 self.reset_interaction()
942 return self.continue_interaction(initial_input)
944 def continue_interaction(self, user_input: str) -> InteractionResult:
945 """
946 Continue the interactive conversation with user input
947 ユーザー入力で対話的会話を継続する
949 Args:
950 user_input: User input for this turn / このターンのユーザー入力
952 Returns:
953 InteractionResult: Interaction result / 対話結果
954 """
955 # Check if max turns reached
956 # 最大ターン数に達したかを確認
957 if self._turn_count >= self.max_turns:
958 return InteractionResult(
959 is_complete=True,
960 content=self._final_result,
961 turn=self._turn_count,
962 remaining_turns=0,
963 success=False,
964 metadata={"error": "Maximum turns reached"}
965 )
967 return self._process_turn(user_input)
969 def _process_turn(self, user_input: str) -> InteractionResult:
970 """
971 Process a single turn of interaction
972 単一ターンの対話を処理する
974 Args:
975 user_input: User input text / ユーザー入力テキスト
977 Returns:
978 InteractionResult: Turn result / ターン結果
979 """
980 try:
981 # Increment turn count
982 # ターン数を増加
983 self._turn_count += 1
985 # Build context with conversation history
986 # 会話履歴でコンテキストを構築
987 context_prompt = self._build_interaction_context()
988 full_input = f"{context_prompt}\n\nCurrent user input: {user_input}"
990 # Run the LLMPipeline
991 # LLMPipelineを実行
992 llm_result = super().run(full_input)
994 # Store interaction in history
995 # 対話を履歴に保存
996 self._store_turn(user_input, llm_result)
998 if not llm_result.success:
999 # Handle LLM execution failure
1000 # LLM実行失敗を処理
1001 return InteractionResult(
1002 is_complete=False,
1003 content=InteractionQuestion(
1004 question="Sorry, I encountered an error. Please try again.",
1005 turn=self._turn_count,
1006 remaining_turns=max(0, self.max_turns - self._turn_count),
1007 metadata=llm_result.metadata
1008 ),
1009 turn=self._turn_count,
1010 remaining_turns=max(0, self.max_turns - self._turn_count),
1011 success=False,
1012 metadata=llm_result.metadata
1013 )
1015 # Check if interaction is complete using completion check function
1016 # 完了チェック関数を使用して対話完了を確認
1017 if self.completion_check(llm_result.content):
1018 # Interaction complete
1019 # 対話完了
1020 self._is_complete = True
1021 self._final_result = llm_result.content
1023 return InteractionResult(
1024 is_complete=True,
1025 content=llm_result.content,
1026 turn=self._turn_count,
1027 remaining_turns=0,
1028 success=True,
1029 metadata=llm_result.metadata
1030 )
1031 else:
1032 # Check if max turns reached after this turn
1033 # このターン後に最大ターン数に達したかを確認
1034 if self._turn_count >= self.max_turns:
1035 # Force completion due to max turns
1036 # 最大ターン数により強制完了
1037 self._is_complete = True
1038 self._final_result = llm_result.content
1040 return InteractionResult(
1041 is_complete=True,
1042 content=llm_result.content,
1043 turn=self._turn_count,
1044 remaining_turns=0,
1045 success=True,
1046 metadata=llm_result.metadata
1047 )
1049 # Continue interaction - format as question
1050 # 対話継続 - 質問としてフォーマット
1051 question_text = self.question_format(
1052 str(llm_result.content),
1053 self._turn_count,
1054 max(0, self.max_turns - self._turn_count)
1055 )
1057 question = InteractionQuestion(
1058 question=question_text,
1059 turn=self._turn_count,
1060 remaining_turns=max(0, self.max_turns - self._turn_count),
1061 metadata=llm_result.metadata
1062 )
1064 return InteractionResult(
1065 is_complete=False,
1066 content=question,
1067 turn=self._turn_count,
1068 remaining_turns=max(0, self.max_turns - self._turn_count),
1069 success=True,
1070 metadata=llm_result.metadata
1071 )
1073 except Exception as e:
1074 # Handle errors gracefully
1075 # エラーを適切に処理
1076 return InteractionResult(
1077 is_complete=False,
1078 content=InteractionQuestion(
1079 question=f"An error occurred: {str(e)}. Please try again.",
1080 turn=self._turn_count,
1081 remaining_turns=max(0, self.max_turns - self._turn_count),
1082 metadata={"error": str(e)}
1083 ),
1084 turn=self._turn_count,
1085 remaining_turns=max(0, self.max_turns - self._turn_count),
1086 success=False,
1087 metadata={"error": str(e)}
1088 )
1090 def _build_interaction_context(self) -> str:
1091 """
1092 Build interaction context from conversation history
1093 会話履歴から対話コンテキストを構築する
1095 Returns:
1096 str: Conversation context / 会話コンテキスト
1097 """
1098 if not self._conversation_history:
1099 return "This is the beginning of the conversation."
1101 context_parts = ["Previous conversation:"]
1102 for i, interaction in enumerate(self._conversation_history, 1):
1103 user_input = interaction.get('user_input', '')
1104 ai_response = str(interaction.get('ai_result', {}).get('content', ''))
1105 context_parts.append(f"{i}. User: {user_input}")
1106 context_parts.append(f" Assistant: {ai_response}")
1108 return "\n".join(context_parts)
1110 def _store_turn(self, user_input: str, llm_result: LLMResult) -> None:
1111 """
1112 Store interaction turn in conversation history
1113 対話ターンを会話履歴に保存する
1115 Args:
1116 user_input: User input / ユーザー入力
1117 llm_result: LLM result / LLM結果
1118 """
1119 # Get timestamp safely
1120 try:
1121 timestamp = asyncio.get_event_loop().time()
1122 except RuntimeError:
1123 # Fallback to regular time if no event loop is running
1124 import time
1125 timestamp = time.time()
1127 interaction = {
1128 'user_input': user_input,
1129 'ai_result': {
1130 'content': llm_result.content,
1131 'success': llm_result.success,
1132 'metadata': llm_result.metadata
1133 },
1134 'turn': self._turn_count,
1135 'timestamp': timestamp
1136 }
1137 self._conversation_history.append(interaction)
1139 def _default_question_format(self, response: str, turn: int, remaining: int) -> str:
1140 """
1141 Default question formatting function
1142 デフォルト質問フォーマット関数
1144 Args:
1145 response: AI response / AI応答
1146 turn: Current turn / 現在のターン
1147 remaining: Remaining turns / 残りターン
1149 Returns:
1150 str: Formatted question / フォーマット済み質問
1151 """
1152 return f"[Turn {turn}] {response}"
1154 def reset_interaction(self) -> None:
1155 """
1156 Reset the interaction session
1157 対話セッションをリセットする
1158 """
1159 self._turn_count = 0
1160 self._conversation_history = []
1161 self._is_complete = False
1162 self._final_result = None
1164 @property
1165 def is_complete(self) -> bool:
1166 """
1167 Check if interaction is complete
1168 対話が完了しているかを確認する
1170 Returns:
1171 bool: True if complete / 完了している場合True
1172 """
1173 return self._is_complete
1175 @property
1176 def current_turn(self) -> int:
1177 """
1178 Get current turn number
1179 現在のターン番号を取得する
1181 Returns:
1182 int: Current turn / 現在のターン
1183 """
1184 return self._turn_count
1186 @property
1187 def remaining_turns(self) -> int:
1188 """
1189 Get remaining turns
1190 残りターン数を取得する
1192 Returns:
1193 int: Remaining turns / 残りターン数
1194 """
1195 return max(0, self.max_turns - self._turn_count)
1197 @property
1198 def interaction_history(self) -> List[Dict[str, Any]]:
1199 """
1200 Get interaction history
1201 対話履歴を取得する
1203 Returns:
1204 List[Dict[str, Any]]: Interaction history / 対話履歴
1205 """
1206 return self._conversation_history.copy()
1208 @property
1209 def final_result(self) -> Any:
1210 """
1211 Get final result if interaction is complete
1212 対話完了の場合は最終結果を取得する
1214 Returns:
1215 Any: Final result or None / 最終結果またはNone
1216 """
1217 return self._final_result if self._is_complete else None
1220def create_simple_interactive_pipeline(
1221 name: str,
1222 instructions: str,
1223 completion_check: Callable[[Any], bool],
1224 max_turns: int = 20,
1225 model: str = "gpt-4o-mini",
1226 **kwargs
1227) -> InteractivePipeline:
1228 """
1229 Create a simple InteractivePipeline with basic configuration
1230 基本設定でシンプルなInteractivePipelineを作成する
1232 Args:
1233 name: Pipeline name / パイプライン名
1234 instructions: Generation instructions / 生成指示
1235 completion_check: Function to check completion / 完了チェック関数
1236 max_turns: Maximum interaction turns / 最大対話ターン数
1237 model: LLM model name / LLMモデル名
1238 **kwargs: Additional arguments / 追加引数
1240 Returns:
1241 InteractivePipeline: Configured pipeline / 設定済みパイプライン
1242 """
1243 return InteractivePipeline(
1244 name=name,
1245 generation_instructions=instructions,
1246 completion_check=completion_check,
1247 max_turns=max_turns,
1248 model=model,
1249 **kwargs
1250 )
1253def create_evaluated_interactive_pipeline(
1254 name: str,
1255 generation_instructions: str,
1256 evaluation_instructions: str,
1257 completion_check: Callable[[Any], bool],
1258 max_turns: int = 20,
1259 model: str = "gpt-4o-mini",
1260 evaluation_model: Optional[str] = None,
1261 threshold: float = 85.0,
1262 **kwargs
1263) -> InteractivePipeline:
1264 """
1265 Create an InteractivePipeline with evaluation capabilities
1266 評価機能付きInteractivePipelineを作成する
1268 Args:
1269 name: Pipeline name / パイプライン名
1270 generation_instructions: Generation instructions / 生成指示
1271 evaluation_instructions: Evaluation instructions / 評価指示
1272 completion_check: Function to check completion / 完了チェック関数
1273 max_turns: Maximum interaction turns / 最大対話ターン数
1274 model: LLM model name / LLMモデル名
1275 evaluation_model: Evaluation model name / 評価モデル名
1276 threshold: Evaluation threshold / 評価閾値
1277 **kwargs: Additional arguments / 追加引数
1279 Returns:
1280 InteractivePipeline: Configured pipeline / 設定済みパイプライン
1281 """
1282 return InteractivePipeline(
1283 name=name,
1284 generation_instructions=generation_instructions,
1285 evaluation_instructions=evaluation_instructions,
1286 completion_check=completion_check,
1287 max_turns=max_turns,
1288 model=model,
1289 evaluation_model=evaluation_model,
1290 threshold=threshold,
1291 **kwargs
1292 )
1295# Utility functions for LLMPipeline (existing)
1296# LLMPipeline用ユーティリティ関数(既存)