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 +3 -0
- gitview/backends/__init__.py +18 -0
- gitview/backends/anthropic_backend.py +66 -0
- gitview/backends/base.py +60 -0
- gitview/backends/ollama_backend.py +96 -0
- gitview/backends/openai_backend.py +71 -0
- gitview/backends/router.py +164 -0
- gitview/chunker.py +367 -0
- gitview/cli.py +332 -0
- gitview/extractor.py +423 -0
- gitview/storyteller.py +352 -0
- gitview/summarizer.py +270 -0
- gitview/writer.py +271 -0
- gitview-0.1.0.dist-info/METADATA +468 -0
- gitview-0.1.0.dist-info/RECORD +19 -0
- gitview-0.1.0.dist-info/WHEEL +5 -0
- gitview-0.1.0.dist-info/entry_points.txt +2 -0
- gitview-0.1.0.dist-info/licenses/LICENSE +21 -0
- gitview-0.1.0.dist-info/top_level.txt +1 -0
gitview/__init__.py
ADDED
|
@@ -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
|
+
)
|
gitview/backends/base.py
ADDED
|
@@ -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)
|