gitview 0.1.0__py3-none-any.whl

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.
gitview/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """GitView - Git history analyzer with LLM-powered narrative generation."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,18 @@
1
+ """LLM backends for GitView."""
2
+
3
+ from .base import BaseLLMBackend, LLMMessage, LLMResponse
4
+ from .anthropic_backend import AnthropicBackend
5
+ from .ollama_backend import OllamaBackend
6
+ from .openai_backend import OpenAIBackend
7
+ from .router import LLMRouter, LLMBackend
8
+
9
+ __all__ = [
10
+ 'BaseLLMBackend',
11
+ 'LLMMessage',
12
+ 'LLMResponse',
13
+ 'AnthropicBackend',
14
+ 'OllamaBackend',
15
+ 'OpenAIBackend',
16
+ 'LLMRouter',
17
+ 'LLMBackend',
18
+ ]
@@ -0,0 +1,66 @@
1
+ """Anthropic Claude backend."""
2
+
3
+ from typing import List, Optional
4
+
5
+ from anthropic import Anthropic
6
+
7
+ from .base import BaseLLMBackend, LLMMessage, LLMResponse
8
+
9
+
10
+ class AnthropicBackend(BaseLLMBackend):
11
+ """Anthropic Claude backend."""
12
+
13
+ def __init__(self, model: str, api_key: str, temperature: float = 0.7, **kwargs):
14
+ """
15
+ Initialize Anthropic backend.
16
+
17
+ Args:
18
+ model: Claude model identifier
19
+ api_key: Anthropic API key
20
+ temperature: Temperature for generation
21
+ **kwargs: Additional parameters
22
+ """
23
+ super().__init__(model, temperature, **kwargs)
24
+ self.client = Anthropic(api_key=api_key)
25
+
26
+ def generate(self, messages: List[LLMMessage], max_tokens: int = 2000,
27
+ **kwargs) -> LLMResponse:
28
+ """
29
+ Generate completion using Claude.
30
+
31
+ Args:
32
+ messages: List of messages
33
+ max_tokens: Maximum tokens to generate
34
+ **kwargs: Additional generation parameters
35
+
36
+ Returns:
37
+ LLMResponse object
38
+ """
39
+ # Convert messages to Anthropic format
40
+ anthropic_messages = [
41
+ {"role": msg.role, "content": msg.content}
42
+ for msg in messages
43
+ ]
44
+
45
+ # Call Anthropic API
46
+ response = self.client.messages.create(
47
+ model=self.model,
48
+ max_tokens=max_tokens,
49
+ temperature=kwargs.get('temperature', self.temperature),
50
+ messages=anthropic_messages
51
+ )
52
+
53
+ # Extract usage info
54
+ usage = {
55
+ 'prompt_tokens': response.usage.input_tokens,
56
+ 'completion_tokens': response.usage.output_tokens,
57
+ 'total_tokens': response.usage.input_tokens + response.usage.output_tokens
58
+ }
59
+
60
+ # Return standardized response
61
+ return LLMResponse(
62
+ content=response.content[0].text,
63
+ model=response.model,
64
+ usage=usage,
65
+ metadata={'stop_reason': response.stop_reason}
66
+ )
@@ -0,0 +1,60 @@
1
+ """Base LLM backend interface."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Dict, List, Any, Optional
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass
9
+ class LLMMessage:
10
+ """Standard message format for LLM APIs."""
11
+
12
+ role: str # "user", "assistant", "system"
13
+ content: str
14
+
15
+
16
+ @dataclass
17
+ class LLMResponse:
18
+ """Standard response format from LLM APIs."""
19
+
20
+ content: str
21
+ model: str
22
+ usage: Optional[Dict[str, int]] = None # Token usage stats
23
+ metadata: Optional[Dict[str, Any]] = None # Additional metadata
24
+
25
+
26
+ class BaseLLMBackend(ABC):
27
+ """Base class for LLM backends."""
28
+
29
+ def __init__(self, model: str, temperature: float = 0.7, **kwargs):
30
+ """
31
+ Initialize backend.
32
+
33
+ Args:
34
+ model: Model identifier
35
+ temperature: Temperature for generation (0.0 - 1.0)
36
+ **kwargs: Additional backend-specific parameters
37
+ """
38
+ self.model = model
39
+ self.temperature = temperature
40
+ self.kwargs = kwargs
41
+
42
+ @abstractmethod
43
+ def generate(self, messages: List[LLMMessage], max_tokens: int = 2000,
44
+ **kwargs) -> LLMResponse:
45
+ """
46
+ Generate completion from messages.
47
+
48
+ Args:
49
+ messages: List of messages
50
+ max_tokens: Maximum tokens to generate
51
+ **kwargs: Additional generation parameters
52
+
53
+ Returns:
54
+ LLMResponse object
55
+ """
56
+ pass
57
+
58
+ def __repr__(self) -> str:
59
+ """String representation."""
60
+ return f"{self.__class__.__name__}(model={self.model}, temperature={self.temperature})"
@@ -0,0 +1,96 @@
1
+ """Ollama backend for local LLM inference."""
2
+
3
+ import json
4
+ from typing import List, Optional
5
+
6
+ import requests
7
+
8
+ from .base import BaseLLMBackend, LLMMessage, LLMResponse
9
+
10
+
11
+ class OllamaBackend(BaseLLMBackend):
12
+ """Ollama local LLM backend."""
13
+
14
+ def __init__(self, model: str, api_url: str = "http://localhost:11434",
15
+ temperature: float = 0.7, **kwargs):
16
+ """
17
+ Initialize Ollama backend.
18
+
19
+ Args:
20
+ model: Ollama model identifier (e.g., "llama3", "mistral")
21
+ api_url: Ollama API URL
22
+ temperature: Temperature for generation
23
+ **kwargs: Additional parameters
24
+ """
25
+ super().__init__(model, temperature, **kwargs)
26
+ self.api_url = api_url.rstrip('/')
27
+
28
+ def generate(self, messages: List[LLMMessage], max_tokens: int = 2000,
29
+ **kwargs) -> LLMResponse:
30
+ """
31
+ Generate completion using Ollama.
32
+
33
+ Args:
34
+ messages: List of messages
35
+ max_tokens: Maximum tokens to generate
36
+ **kwargs: Additional generation parameters
37
+
38
+ Returns:
39
+ LLMResponse object
40
+ """
41
+ # Convert messages to Ollama format
42
+ ollama_messages = [
43
+ {"role": msg.role, "content": msg.content}
44
+ for msg in messages
45
+ ]
46
+
47
+ # Prepare request
48
+ payload = {
49
+ "model": self.model,
50
+ "messages": ollama_messages,
51
+ "stream": False,
52
+ "options": {
53
+ "temperature": kwargs.get('temperature', self.temperature),
54
+ "num_predict": max_tokens,
55
+ }
56
+ }
57
+
58
+ # Call Ollama API
59
+ try:
60
+ response = requests.post(
61
+ f"{self.api_url}/api/chat",
62
+ json=payload,
63
+ timeout=120 # 2 minute timeout for local inference
64
+ )
65
+ response.raise_for_status()
66
+ data = response.json()
67
+
68
+ # Extract content
69
+ content = data.get('message', {}).get('content', '')
70
+
71
+ # Extract usage if available
72
+ usage = None
73
+ if 'prompt_eval_count' in data and 'eval_count' in data:
74
+ usage = {
75
+ 'prompt_tokens': data.get('prompt_eval_count', 0),
76
+ 'completion_tokens': data.get('eval_count', 0),
77
+ 'total_tokens': data.get('prompt_eval_count', 0) + data.get('eval_count', 0)
78
+ }
79
+
80
+ # Return standardized response
81
+ return LLMResponse(
82
+ content=content,
83
+ model=data.get('model', self.model),
84
+ usage=usage,
85
+ metadata={
86
+ 'done': data.get('done', False),
87
+ 'total_duration': data.get('total_duration'),
88
+ }
89
+ )
90
+
91
+ except requests.exceptions.RequestException as e:
92
+ raise RuntimeError(
93
+ f"Ollama API error: {e}\n"
94
+ f"Make sure Ollama is running at {self.api_url}\n"
95
+ f"Start it with: ollama serve"
96
+ ) from e
@@ -0,0 +1,71 @@
1
+ """OpenAI backend."""
2
+
3
+ from typing import List, Optional
4
+
5
+ from openai import OpenAI
6
+
7
+ from .base import BaseLLMBackend, LLMMessage, LLMResponse
8
+
9
+
10
+ class OpenAIBackend(BaseLLMBackend):
11
+ """OpenAI GPT backend."""
12
+
13
+ def __init__(self, model: str, api_key: str, temperature: float = 0.7, **kwargs):
14
+ """
15
+ Initialize OpenAI backend.
16
+
17
+ Args:
18
+ model: OpenAI model identifier (e.g., "gpt-4", "gpt-3.5-turbo")
19
+ api_key: OpenAI API key
20
+ temperature: Temperature for generation
21
+ **kwargs: Additional parameters
22
+ """
23
+ super().__init__(model, temperature, **kwargs)
24
+ self.client = OpenAI(api_key=api_key)
25
+
26
+ def generate(self, messages: List[LLMMessage], max_tokens: int = 2000,
27
+ **kwargs) -> LLMResponse:
28
+ """
29
+ Generate completion using OpenAI.
30
+
31
+ Args:
32
+ messages: List of messages
33
+ max_tokens: Maximum tokens to generate
34
+ **kwargs: Additional generation parameters
35
+
36
+ Returns:
37
+ LLMResponse object
38
+ """
39
+ # Convert messages to OpenAI format
40
+ openai_messages = [
41
+ {"role": msg.role, "content": msg.content}
42
+ for msg in messages
43
+ ]
44
+
45
+ # Call OpenAI API
46
+ response = self.client.chat.completions.create(
47
+ model=self.model,
48
+ messages=openai_messages,
49
+ max_tokens=max_tokens,
50
+ temperature=kwargs.get('temperature', self.temperature)
51
+ )
52
+
53
+ # Extract usage info
54
+ usage = None
55
+ if response.usage:
56
+ usage = {
57
+ 'prompt_tokens': response.usage.prompt_tokens,
58
+ 'completion_tokens': response.usage.completion_tokens,
59
+ 'total_tokens': response.usage.total_tokens
60
+ }
61
+
62
+ # Return standardized response
63
+ return LLMResponse(
64
+ content=response.choices[0].message.content,
65
+ model=response.model,
66
+ usage=usage,
67
+ metadata={
68
+ 'finish_reason': response.choices[0].finish_reason,
69
+ 'created': response.created
70
+ }
71
+ )
@@ -0,0 +1,164 @@
1
+ """Simplified LLM router for GitView."""
2
+
3
+ import os
4
+ from enum import Enum
5
+ from typing import Optional, List
6
+
7
+ from .base import BaseLLMBackend, LLMMessage
8
+ from .anthropic_backend import AnthropicBackend
9
+ from .ollama_backend import OllamaBackend
10
+ from .openai_backend import OpenAIBackend
11
+
12
+
13
+ class LLMBackend(str, Enum):
14
+ """Supported LLM backends."""
15
+
16
+ ANTHROPIC = "anthropic"
17
+ OPENAI = "openai"
18
+ OLLAMA = "ollama"
19
+
20
+
21
+ class LLMRouter:
22
+ """Routes LLM requests to appropriate backend."""
23
+
24
+ # Default models for each backend
25
+ DEFAULT_MODELS = {
26
+ LLMBackend.ANTHROPIC: "claude-sonnet-4-5-20250929",
27
+ LLMBackend.OPENAI: "gpt-4",
28
+ LLMBackend.OLLAMA: "llama3",
29
+ }
30
+
31
+ def __init__(self, backend: Optional[str] = None, model: Optional[str] = None,
32
+ api_key: Optional[str] = None, **kwargs):
33
+ """
34
+ Initialize LLM router.
35
+
36
+ Args:
37
+ backend: Backend to use ('anthropic', 'openai', 'ollama')
38
+ model: Model identifier (uses defaults if not specified)
39
+ api_key: API key for the backend (if required)
40
+ **kwargs: Additional backend-specific parameters
41
+ """
42
+ # Determine backend
43
+ if backend:
44
+ self.backend_type = LLMBackend(backend.lower())
45
+ else:
46
+ # Auto-detect from environment
47
+ if os.environ.get('ANTHROPIC_API_KEY'):
48
+ self.backend_type = LLMBackend.ANTHROPIC
49
+ elif os.environ.get('OPENAI_API_KEY'):
50
+ self.backend_type = LLMBackend.OPENAI
51
+ else:
52
+ self.backend_type = LLMBackend.OLLAMA
53
+
54
+ # Determine model
55
+ self.model = model or self.DEFAULT_MODELS[self.backend_type]
56
+
57
+ # Determine API key
58
+ if api_key:
59
+ self.api_key = api_key
60
+ else:
61
+ # Try to get from environment
62
+ if self.backend_type == LLMBackend.ANTHROPIC:
63
+ self.api_key = os.environ.get('ANTHROPIC_API_KEY')
64
+ elif self.backend_type == LLMBackend.OPENAI:
65
+ self.api_key = os.environ.get('OPENAI_API_KEY')
66
+ else:
67
+ self.api_key = None # Not needed for Ollama
68
+
69
+ # Additional parameters
70
+ self.kwargs = kwargs
71
+
72
+ # Create backend
73
+ self._backend: Optional[BaseLLMBackend] = None
74
+
75
+ def _get_backend(self) -> BaseLLMBackend:
76
+ """Get or create backend instance."""
77
+ if self._backend is None:
78
+ temperature = self.kwargs.get('temperature', 0.7)
79
+
80
+ if self.backend_type == LLMBackend.ANTHROPIC:
81
+ if not self.api_key:
82
+ raise ValueError(
83
+ "Anthropic API key required. Set ANTHROPIC_API_KEY environment variable "
84
+ "or pass api_key parameter."
85
+ )
86
+ self._backend = AnthropicBackend(
87
+ model=self.model,
88
+ api_key=self.api_key,
89
+ temperature=temperature
90
+ )
91
+
92
+ elif self.backend_type == LLMBackend.OPENAI:
93
+ if not self.api_key:
94
+ raise ValueError(
95
+ "OpenAI API key required. Set OPENAI_API_KEY environment variable "
96
+ "or pass api_key parameter."
97
+ )
98
+ self._backend = OpenAIBackend(
99
+ model=self.model,
100
+ api_key=self.api_key,
101
+ temperature=temperature
102
+ )
103
+
104
+ elif self.backend_type == LLMBackend.OLLAMA:
105
+ ollama_url = self.kwargs.get('ollama_url', 'http://localhost:11434')
106
+ self._backend = OllamaBackend(
107
+ model=self.model,
108
+ api_url=ollama_url,
109
+ temperature=temperature
110
+ )
111
+
112
+ return self._backend
113
+
114
+ def generate(self, messages: List[LLMMessage], max_tokens: int = 2000, **kwargs):
115
+ """
116
+ Generate completion.
117
+
118
+ Args:
119
+ messages: List of messages
120
+ max_tokens: Maximum tokens to generate
121
+ **kwargs: Additional generation parameters
122
+
123
+ Returns:
124
+ LLMResponse object
125
+ """
126
+ backend = self._get_backend()
127
+ return backend.generate(messages, max_tokens, **kwargs)
128
+
129
+ def generate_text(self, prompt: str, max_tokens: int = 2000, **kwargs) -> str:
130
+ """
131
+ Generate completion from a simple text prompt.
132
+
133
+ Args:
134
+ prompt: Text prompt
135
+ max_tokens: Maximum tokens to generate
136
+ **kwargs: Additional generation parameters
137
+
138
+ Returns:
139
+ Generated text
140
+ """
141
+ messages = [LLMMessage(role="user", content=prompt)]
142
+ response = self.generate(messages, max_tokens, **kwargs)
143
+ return response.content
144
+
145
+ def __repr__(self) -> str:
146
+ """String representation."""
147
+ return f"LLMRouter(backend={self.backend_type.value}, model={self.model})"
148
+
149
+
150
+ def create_router(backend: Optional[str] = None, model: Optional[str] = None,
151
+ api_key: Optional[str] = None, **kwargs) -> LLMRouter:
152
+ """
153
+ Create an LLM router.
154
+
155
+ Args:
156
+ backend: Backend to use ('anthropic', 'openai', 'ollama')
157
+ model: Model identifier
158
+ api_key: API key for the backend
159
+ **kwargs: Additional backend parameters
160
+
161
+ Returns:
162
+ LLMRouter instance
163
+ """
164
+ return LLMRouter(backend=backend, model=model, api_key=api_key, **kwargs)