switchforge 1.0.0__tar.gz → 1.1.0__tar.gz

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.
Files changed (41) hide show
  1. {switchforge-1.0.0 → switchforge-1.1.0}/PKG-INFO +5 -4
  2. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/ai/provider.py +35 -36
  3. switchforge-1.1.0/forge_core/ai/structured.py +62 -0
  4. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/utils/tokens.py +16 -2
  5. {switchforge-1.0.0 → switchforge-1.1.0}/pyproject.toml +4 -4
  6. switchforge-1.0.0/forge_core/ai/structured.py +0 -68
  7. {switchforge-1.0.0 → switchforge-1.1.0}/.gitignore +0 -0
  8. {switchforge-1.0.0 → switchforge-1.1.0}/README.md +0 -0
  9. {switchforge-1.0.0 → switchforge-1.1.0}/forge-core.spec +0 -0
  10. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/__init__.py +0 -0
  11. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/__main__.py +0 -0
  12. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/ai/__init__.py +0 -0
  13. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/ai/prompts.py +0 -0
  14. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/auth.py +0 -0
  15. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/cli.py +0 -0
  16. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/config.py +0 -0
  17. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/core/__init__.py +0 -0
  18. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/core/agent_manager.py +0 -0
  19. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/core/coverage.py +0 -0
  20. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/core/file_manager.py +0 -0
  21. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/models/__init__.py +0 -0
  22. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/models/config.py +0 -0
  23. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/models/dto.py +0 -0
  24. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/models/project.py +0 -0
  25. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/models/test_result.py +0 -0
  26. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/orchestrator.py +0 -0
  27. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/phases/__init__.py +0 -0
  28. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/phases/analyze_project.py +0 -0
  29. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/phases/audit_tests.py +0 -0
  30. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/phases/compile_fix.py +0 -0
  31. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/phases/coverage_report.py +0 -0
  32. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/phases/detect_stack.py +0 -0
  33. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/phases/exclusion_scan.py +0 -0
  34. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/phases/fix_broken.py +0 -0
  35. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/phases/generate_tests.py +0 -0
  36. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/phases/journey_mapping.py +0 -0
  37. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/phases/self_learn.py +0 -0
  38. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/utils/__init__.py +0 -0
  39. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/utils/logger.py +0 -0
  40. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/utils/reporter.py +0 -0
  41. {switchforge-1.0.0 → switchforge-1.1.0}/forge_core/utils/shell.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: switchforge
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: AI-powered backend test generation engine
5
5
  Project-URL: Homepage, https://theswitchcompany.online/products/forge/core
6
6
  Project-URL: Repository, https://github.com/switchcompany/forge-core
@@ -18,18 +18,19 @@ Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Topic :: Software Development :: Testing
19
19
  Requires-Python: >=3.10
20
20
  Requires-Dist: httpx>=0.27.0
21
- Requires-Dist: instructor>=1.3.0
22
- Requires-Dist: litellm>=1.40.0
21
+ Requires-Dist: openai>=1.30.0
23
22
  Requires-Dist: pydantic>=2.7.0
24
23
  Requires-Dist: pyyaml>=6.0
25
24
  Requires-Dist: rich>=13.7.0
26
- Requires-Dist: tiktoken>=0.7.0
27
25
  Requires-Dist: typer>=0.12.0
26
+ Provides-Extra: accurate-tokens
27
+ Requires-Dist: tiktoken>=0.7.0; extra == 'accurate-tokens'
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: pyinstaller>=6.0; extra == 'dev'
30
30
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
31
31
  Requires-Dist: pytest>=8.0; extra == 'dev'
32
32
  Requires-Dist: ruff>=0.5.0; extra == 'dev'
33
+ Requires-Dist: tiktoken>=0.7.0; extra == 'dev'
33
34
  Description-Content-Type: text/markdown
34
35
 
35
36
  # Switchforge (Forge Core CLI)
@@ -1,27 +1,44 @@
1
- """LiteLLM-based AI provider — unified interface for 100+ models."""
1
+ """AI provider — direct OpenAI SDK, zero native dependencies."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
5
6
  from typing import Any
6
7
 
7
- import litellm
8
+ from openai import OpenAI
8
9
 
9
10
  from forge_core.models.config import AIConfig, AIProvider
10
11
  from forge_core.utils import logger
11
12
  from forge_core.utils.tokens import count_tokens
12
13
 
13
- # Suppress LiteLLM's verbose logging
14
- litellm.suppress_debug_info = True
14
+
15
+ def _get_client(config: AIConfig) -> OpenAI:
16
+ """Create an OpenAI-compatible client for any provider."""
17
+ api_key = config.api_key or os.environ.get("OPENAI_API_KEY", "")
18
+
19
+ if config.provider == AIProvider.ANTHROPIC:
20
+ base_url = config.base_url or "https://api.anthropic.com/v1"
21
+ api_key = config.api_key or os.environ.get("ANTHROPIC_API_KEY", "")
22
+ elif config.provider == AIProvider.OLLAMA:
23
+ base_url = config.base_url or "http://localhost:11434/v1"
24
+ api_key = "ollama"
25
+ elif config.provider == AIProvider.AZURE:
26
+ base_url = config.base_url or os.environ.get("AZURE_OPENAI_ENDPOINT", "")
27
+ api_key = config.api_key or os.environ.get("AZURE_OPENAI_API_KEY", "")
28
+ elif config.base_url:
29
+ base_url = config.base_url
30
+ else:
31
+ base_url = None # default OpenAI
32
+
33
+ kwargs: dict[str, Any] = {"api_key": api_key}
34
+ if base_url:
35
+ kwargs["base_url"] = base_url
36
+
37
+ return OpenAI(**kwargs)
15
38
 
16
39
 
17
40
  def _resolve_model(config: AIConfig) -> str:
18
- """Resolve the model string for LiteLLM based on provider."""
19
- if config.provider == AIProvider.OLLAMA:
20
- return f"ollama/{config.model}"
21
- if config.provider == AIProvider.AZURE:
22
- return f"azure/{config.model}"
23
- if config.provider == AIProvider.BEDROCK:
24
- return f"bedrock/{config.model}"
41
+ """Resolve the model name."""
25
42
  return config.model
26
43
 
27
44
 
@@ -32,19 +49,10 @@ def complete(
32
49
  json_mode: bool = False,
33
50
  max_tokens: int | None = None,
34
51
  ) -> str:
35
- """Send a completion request to the configured AI provider.
36
-
37
- Args:
38
- config: AI provider configuration.
39
- system_prompt: System-level instructions.
40
- user_prompt: User message with code/context.
41
- json_mode: If True, request JSON response format.
42
- max_tokens: Override max tokens for this call.
43
-
44
- Returns:
45
- The AI's response text.
46
- """
52
+ """Send a completion request to the configured AI provider."""
47
53
  model = _resolve_model(config)
54
+ client = _get_client(config)
55
+
48
56
  messages = [
49
57
  {"role": "system", "content": system_prompt},
50
58
  {"role": "user", "content": user_prompt},
@@ -57,19 +65,14 @@ def complete(
57
65
  "max_tokens": max_tokens or config.max_tokens,
58
66
  }
59
67
 
60
- if config.api_key:
61
- kwargs["api_key"] = config.api_key
62
- if config.base_url:
63
- kwargs["api_base"] = config.base_url
64
68
  if json_mode:
65
69
  kwargs["response_format"] = {"type": "json_object"}
66
70
 
67
- # Log token usage
68
71
  input_tokens = count_tokens(system_prompt + user_prompt, config.model)
69
72
  logger.info(f"AI call → {model} ({input_tokens} input tokens)")
70
73
 
71
74
  try:
72
- response = litellm.completion(**kwargs)
75
+ response = client.chat.completions.create(**kwargs)
73
76
  content = response.choices[0].message.content or ""
74
77
 
75
78
  output_tokens = count_tokens(content, config.model)
@@ -88,11 +91,9 @@ def complete_with_fallback(
88
91
  fallback_models: list[str] | None = None,
89
92
  json_mode: bool = False,
90
93
  ) -> str:
91
- """Try primary model, fall back to alternatives on failure.
92
-
93
- Useful for complex classes where one model might fail.
94
- """
94
+ """Try primary model, fall back to alternatives on failure."""
95
95
  models = [_resolve_model(config)] + (fallback_models or [])
96
+ client = _get_client(config)
96
97
 
97
98
  last_error = None
98
99
  for model in models:
@@ -106,12 +107,10 @@ def complete_with_fallback(
106
107
  "temperature": config.temperature,
107
108
  "max_tokens": config.max_tokens,
108
109
  }
109
- if config.api_key:
110
- kwargs["api_key"] = config.api_key
111
110
  if json_mode:
112
111
  kwargs["response_format"] = {"type": "json_object"}
113
112
 
114
- response = litellm.completion(**kwargs)
113
+ response = client.chat.completions.create(**kwargs)
115
114
  return response.choices[0].message.content or ""
116
115
  except Exception as e:
117
116
  last_error = e
@@ -0,0 +1,62 @@
1
+ """Structured AI outputs using OpenAI JSON mode + pydantic parsing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import TypeVar
7
+
8
+ from pydantic import BaseModel
9
+
10
+ from forge_core.models.config import AIConfig
11
+ from forge_core.ai.provider import complete
12
+ from forge_core.utils import logger
13
+
14
+ T = TypeVar("T", bound=BaseModel)
15
+
16
+
17
+ def extract(
18
+ config: AIConfig,
19
+ system_prompt: str,
20
+ user_prompt: str,
21
+ response_model: type[T],
22
+ max_retries: int = 2,
23
+ ) -> T:
24
+ """Extract structured data from AI using JSON mode + pydantic.
25
+
26
+ Forces the AI to return data matching a pydantic model schema.
27
+ Retries on validation failure.
28
+ """
29
+ schema = response_model.model_json_schema()
30
+ structured_prompt = (
31
+ f"{system_prompt}\n\n"
32
+ f"You MUST respond with valid JSON matching this schema:\n"
33
+ f"```json\n{json.dumps(schema, indent=2)}\n```\n"
34
+ f"Respond ONLY with the JSON object, no other text."
35
+ )
36
+
37
+ logger.info(f"Structured extraction → {config.model} → {response_model.__name__}")
38
+
39
+ last_error = None
40
+ for attempt in range(max_retries + 1):
41
+ try:
42
+ raw = complete(config, structured_prompt, user_prompt, json_mode=True)
43
+ # Strip markdown code fences if present
44
+ cleaned = raw.strip()
45
+ if cleaned.startswith("```"):
46
+ cleaned = cleaned.split("\n", 1)[1] if "\n" in cleaned else cleaned
47
+ if cleaned.endswith("```"):
48
+ cleaned = cleaned[:-3]
49
+ cleaned = cleaned.strip()
50
+
51
+ result = response_model.model_validate_json(cleaned)
52
+ logger.success(f"Extracted {response_model.__name__} successfully")
53
+ return result
54
+ except Exception as e:
55
+ last_error = e
56
+ if attempt < max_retries:
57
+ logger.warn(f"Extraction attempt {attempt + 1} failed: {e}, retrying...")
58
+ continue
59
+
60
+ raise RuntimeError(
61
+ f"Structured extraction failed after {max_retries + 1} attempts: {last_error}"
62
+ )
@@ -1,12 +1,23 @@
1
- """Token counting and prompt size management using tiktoken."""
1
+ """Token counting and prompt size management."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import tiktoken
5
+ try:
6
+ import tiktoken
7
+ _HAS_TIKTOKEN = True
8
+ except ImportError:
9
+ _HAS_TIKTOKEN = False
10
+
11
+
12
+ def _estimate_tokens(text: str) -> int:
13
+ """Rough token estimate (~4 chars per token) when tiktoken is unavailable."""
14
+ return len(text) // 4 + 1
6
15
 
7
16
 
8
17
  def count_tokens(text: str, model: str = "gpt-4o") -> int:
9
18
  """Count tokens in text for a given model."""
19
+ if not _HAS_TIKTOKEN:
20
+ return _estimate_tokens(text)
10
21
  try:
11
22
  enc = tiktoken.encoding_for_model(model)
12
23
  except KeyError:
@@ -16,6 +27,9 @@ def count_tokens(text: str, model: str = "gpt-4o") -> int:
16
27
 
17
28
  def truncate_to_tokens(text: str, max_tokens: int, model: str = "gpt-4o") -> str:
18
29
  """Truncate text to fit within max_tokens."""
30
+ if not _HAS_TIKTOKEN:
31
+ char_limit = max_tokens * 4
32
+ return text[:char_limit] if len(text) > char_limit else text
19
33
  try:
20
34
  enc = tiktoken.encoding_for_model(model)
21
35
  except KeyError:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "switchforge"
7
- version = "1.0.0"
7
+ version = "1.1.0"
8
8
  description = "AI-powered backend test generation engine"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -25,22 +25,22 @@ classifiers = [
25
25
  ]
26
26
 
27
27
  dependencies = [
28
- "litellm>=1.40.0",
29
- "instructor>=1.3.0",
28
+ "openai>=1.30.0",
30
29
  "typer>=0.12.0",
31
30
  "rich>=13.7.0",
32
31
  "pyyaml>=6.0",
33
- "tiktoken>=0.7.0",
34
32
  "httpx>=0.27.0",
35
33
  "pydantic>=2.7.0",
36
34
  ]
37
35
 
38
36
  [project.optional-dependencies]
37
+ accurate-tokens = ["tiktoken>=0.7.0"]
39
38
  dev = [
40
39
  "pytest>=8.0",
41
40
  "pytest-asyncio>=0.23",
42
41
  "ruff>=0.5.0",
43
42
  "pyinstaller>=6.0",
43
+ "tiktoken>=0.7.0",
44
44
  ]
45
45
 
46
46
  [project.scripts]
@@ -1,68 +0,0 @@
1
- """Structured AI outputs using instructor + pydantic."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import Any, TypeVar
6
-
7
- import instructor
8
- import litellm
9
- from pydantic import BaseModel
10
-
11
- from forge_core.models.config import AIConfig
12
- from forge_core.ai.provider import _resolve_model
13
- from forge_core.utils import logger
14
-
15
- T = TypeVar("T", bound=BaseModel)
16
-
17
-
18
- def extract(
19
- config: AIConfig,
20
- system_prompt: str,
21
- user_prompt: str,
22
- response_model: type[T],
23
- max_retries: int = 2,
24
- ) -> T:
25
- """Extract structured data from AI using instructor.
26
-
27
- Forces the AI to return data matching a pydantic model schema.
28
- Retries on validation failure.
29
-
30
- Args:
31
- config: AI provider configuration.
32
- system_prompt: System-level instructions.
33
- user_prompt: User message with code/context.
34
- response_model: Pydantic model class to extract.
35
- max_retries: Number of retries on validation failure.
36
-
37
- Returns:
38
- An instance of response_model populated by the AI.
39
- """
40
- model = _resolve_model(config)
41
-
42
- client = instructor.from_litellm(litellm.completion)
43
-
44
- kwargs: dict[str, Any] = {
45
- "model": model,
46
- "messages": [
47
- {"role": "system", "content": system_prompt},
48
- {"role": "user", "content": user_prompt},
49
- ],
50
- "temperature": config.temperature,
51
- "max_retries": max_retries,
52
- "response_model": response_model,
53
- }
54
-
55
- if config.api_key:
56
- kwargs["api_key"] = config.api_key
57
- if config.base_url:
58
- kwargs["api_base"] = config.base_url
59
-
60
- logger.info(f"Structured extraction → {model} → {response_model.__name__}")
61
-
62
- try:
63
- result = client.chat.completions.create(**kwargs)
64
- logger.success(f"Extracted {response_model.__name__} successfully")
65
- return result
66
- except Exception as e:
67
- logger.error(f"Structured extraction failed: {e}")
68
- raise
File without changes
File without changes
File without changes