vbagent 0.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 (147) hide show
  1. vbagent-0.1.0/PKG-INFO +21 -0
  2. vbagent-0.1.0/README.md +0 -0
  3. vbagent-0.1.0/pyproject.toml +29 -0
  4. vbagent-0.1.0/vbagent/__init__.py +3 -0
  5. vbagent-0.1.0/vbagent/agents/__init__.py +127 -0
  6. vbagent-0.1.0/vbagent/agents/__pycache__/__init__.cpython-312.pyc +0 -0
  7. vbagent-0.1.0/vbagent/agents/__pycache__/alternate.cpython-312.pyc +0 -0
  8. vbagent-0.1.0/vbagent/agents/__pycache__/base.cpython-312.pyc +0 -0
  9. vbagent-0.1.0/vbagent/agents/__pycache__/clarity_checker.cpython-312.pyc +0 -0
  10. vbagent-0.1.0/vbagent/agents/__pycache__/classifier.cpython-312.pyc +0 -0
  11. vbagent-0.1.0/vbagent/agents/__pycache__/converter.cpython-312.pyc +0 -0
  12. vbagent-0.1.0/vbagent/agents/__pycache__/grammar_checker.cpython-312.pyc +0 -0
  13. vbagent-0.1.0/vbagent/agents/__pycache__/idea.cpython-312.pyc +0 -0
  14. vbagent-0.1.0/vbagent/agents/__pycache__/multi_variant.cpython-312.pyc +0 -0
  15. vbagent-0.1.0/vbagent/agents/__pycache__/reviewer.cpython-312.pyc +0 -0
  16. vbagent-0.1.0/vbagent/agents/__pycache__/scanner.cpython-312.pyc +0 -0
  17. vbagent-0.1.0/vbagent/agents/__pycache__/selector.cpython-312.pyc +0 -0
  18. vbagent-0.1.0/vbagent/agents/__pycache__/solution_checker.cpython-312.pyc +0 -0
  19. vbagent-0.1.0/vbagent/agents/__pycache__/tikz.cpython-312.pyc +0 -0
  20. vbagent-0.1.0/vbagent/agents/__pycache__/tikz_checker.cpython-312.pyc +0 -0
  21. vbagent-0.1.0/vbagent/agents/__pycache__/variant.cpython-312.pyc +0 -0
  22. vbagent-0.1.0/vbagent/agents/alternate.py +178 -0
  23. vbagent-0.1.0/vbagent/agents/base.py +214 -0
  24. vbagent-0.1.0/vbagent/agents/clarity_checker.py +113 -0
  25. vbagent-0.1.0/vbagent/agents/classifier.py +39 -0
  26. vbagent-0.1.0/vbagent/agents/converter.py +115 -0
  27. vbagent-0.1.0/vbagent/agents/grammar_checker.py +113 -0
  28. vbagent-0.1.0/vbagent/agents/idea.py +135 -0
  29. vbagent-0.1.0/vbagent/agents/multi_variant.py +68 -0
  30. vbagent-0.1.0/vbagent/agents/reviewer.py +364 -0
  31. vbagent-0.1.0/vbagent/agents/scanner.py +144 -0
  32. vbagent-0.1.0/vbagent/agents/selector.py +143 -0
  33. vbagent-0.1.0/vbagent/agents/solution_checker.py +116 -0
  34. vbagent-0.1.0/vbagent/agents/tikz.py +240 -0
  35. vbagent-0.1.0/vbagent/agents/tikz_checker.py +153 -0
  36. vbagent-0.1.0/vbagent/agents/variant.py +185 -0
  37. vbagent-0.1.0/vbagent/cli/__init__.py +1 -0
  38. vbagent-0.1.0/vbagent/cli/__pycache__/__init__.cpython-312.pyc +0 -0
  39. vbagent-0.1.0/vbagent/cli/__pycache__/alternate.cpython-312.pyc +0 -0
  40. vbagent-0.1.0/vbagent/cli/__pycache__/batch.cpython-312.pyc +0 -0
  41. vbagent-0.1.0/vbagent/cli/__pycache__/check.cpython-312.pyc +0 -0
  42. vbagent-0.1.0/vbagent/cli/__pycache__/classify.cpython-312.pyc +0 -0
  43. vbagent-0.1.0/vbagent/cli/__pycache__/common.cpython-312.pyc +0 -0
  44. vbagent-0.1.0/vbagent/cli/__pycache__/config.cpython-312.pyc +0 -0
  45. vbagent-0.1.0/vbagent/cli/__pycache__/convert.cpython-312.pyc +0 -0
  46. vbagent-0.1.0/vbagent/cli/__pycache__/idea.cpython-312.pyc +0 -0
  47. vbagent-0.1.0/vbagent/cli/__pycache__/main.cpython-312.pyc +0 -0
  48. vbagent-0.1.0/vbagent/cli/__pycache__/process.cpython-312.pyc +0 -0
  49. vbagent-0.1.0/vbagent/cli/__pycache__/ref.cpython-312.pyc +0 -0
  50. vbagent-0.1.0/vbagent/cli/__pycache__/scan.cpython-312.pyc +0 -0
  51. vbagent-0.1.0/vbagent/cli/__pycache__/tikz.cpython-312.pyc +0 -0
  52. vbagent-0.1.0/vbagent/cli/__pycache__/variant.cpython-312.pyc +0 -0
  53. vbagent-0.1.0/vbagent/cli/alternate.py +176 -0
  54. vbagent-0.1.0/vbagent/cli/batch.py +633 -0
  55. vbagent-0.1.0/vbagent/cli/check.py +2962 -0
  56. vbagent-0.1.0/vbagent/cli/classify.py +112 -0
  57. vbagent-0.1.0/vbagent/cli/common.py +779 -0
  58. vbagent-0.1.0/vbagent/cli/config.py +191 -0
  59. vbagent-0.1.0/vbagent/cli/convert.py +248 -0
  60. vbagent-0.1.0/vbagent/cli/idea.py +165 -0
  61. vbagent-0.1.0/vbagent/cli/main.py +91 -0
  62. vbagent-0.1.0/vbagent/cli/process.py +1067 -0
  63. vbagent-0.1.0/vbagent/cli/ref.py +617 -0
  64. vbagent-0.1.0/vbagent/cli/scan.py +133 -0
  65. vbagent-0.1.0/vbagent/cli/tikz.py +125 -0
  66. vbagent-0.1.0/vbagent/cli/variant.py +271 -0
  67. vbagent-0.1.0/vbagent/config.py +291 -0
  68. vbagent-0.1.0/vbagent/models/__init__.py +57 -0
  69. vbagent-0.1.0/vbagent/models/__pycache__/__init__.cpython-312.pyc +0 -0
  70. vbagent-0.1.0/vbagent/models/__pycache__/batch.cpython-312.pyc +0 -0
  71. vbagent-0.1.0/vbagent/models/__pycache__/classification.cpython-312.pyc +0 -0
  72. vbagent-0.1.0/vbagent/models/__pycache__/diff.cpython-312.pyc +0 -0
  73. vbagent-0.1.0/vbagent/models/__pycache__/idea.cpython-312.pyc +0 -0
  74. vbagent-0.1.0/vbagent/models/__pycache__/pipeline.cpython-312.pyc +0 -0
  75. vbagent-0.1.0/vbagent/models/__pycache__/review.cpython-312.pyc +0 -0
  76. vbagent-0.1.0/vbagent/models/__pycache__/scan.cpython-312.pyc +0 -0
  77. vbagent-0.1.0/vbagent/models/__pycache__/version_store.cpython-312.pyc +0 -0
  78. vbagent-0.1.0/vbagent/models/batch.py +380 -0
  79. vbagent-0.1.0/vbagent/models/classification.py +37 -0
  80. vbagent-0.1.0/vbagent/models/diff.py +416 -0
  81. vbagent-0.1.0/vbagent/models/idea.py +15 -0
  82. vbagent-0.1.0/vbagent/models/pipeline.py +20 -0
  83. vbagent-0.1.0/vbagent/models/review.py +69 -0
  84. vbagent-0.1.0/vbagent/models/scan.py +13 -0
  85. vbagent-0.1.0/vbagent/models/version_store.py +955 -0
  86. vbagent-0.1.0/vbagent/prompts/__init__.py +1 -0
  87. vbagent-0.1.0/vbagent/prompts/__pycache__/__init__.cpython-312.pyc +0 -0
  88. vbagent-0.1.0/vbagent/prompts/__pycache__/alternate.cpython-312.pyc +0 -0
  89. vbagent-0.1.0/vbagent/prompts/__pycache__/clarity_checker.cpython-312.pyc +0 -0
  90. vbagent-0.1.0/vbagent/prompts/__pycache__/classifier.cpython-312.pyc +0 -0
  91. vbagent-0.1.0/vbagent/prompts/__pycache__/converter.cpython-312.pyc +0 -0
  92. vbagent-0.1.0/vbagent/prompts/__pycache__/grammar_checker.cpython-312.pyc +0 -0
  93. vbagent-0.1.0/vbagent/prompts/__pycache__/idea.cpython-312.pyc +0 -0
  94. vbagent-0.1.0/vbagent/prompts/__pycache__/reviewer.cpython-312.pyc +0 -0
  95. vbagent-0.1.0/vbagent/prompts/__pycache__/solution_checker.cpython-312.pyc +0 -0
  96. vbagent-0.1.0/vbagent/prompts/__pycache__/tikz.cpython-312.pyc +0 -0
  97. vbagent-0.1.0/vbagent/prompts/__pycache__/tikz_checker.cpython-312.pyc +0 -0
  98. vbagent-0.1.0/vbagent/prompts/alternate.py +110 -0
  99. vbagent-0.1.0/vbagent/prompts/clarity_checker.py +58 -0
  100. vbagent-0.1.0/vbagent/prompts/classifier.py +31 -0
  101. vbagent-0.1.0/vbagent/prompts/converter.py +125 -0
  102. vbagent-0.1.0/vbagent/prompts/grammar_checker.py +58 -0
  103. vbagent-0.1.0/vbagent/prompts/idea.py +142 -0
  104. vbagent-0.1.0/vbagent/prompts/reviewer.py +140 -0
  105. vbagent-0.1.0/vbagent/prompts/scanner/__init__.py +71 -0
  106. vbagent-0.1.0/vbagent/prompts/scanner/__pycache__/__init__.cpython-312.pyc +0 -0
  107. vbagent-0.1.0/vbagent/prompts/scanner/__pycache__/assertion_reason.cpython-312.pyc +0 -0
  108. vbagent-0.1.0/vbagent/prompts/scanner/__pycache__/common.cpython-312.pyc +0 -0
  109. vbagent-0.1.0/vbagent/prompts/scanner/__pycache__/match.cpython-312.pyc +0 -0
  110. vbagent-0.1.0/vbagent/prompts/scanner/__pycache__/mcq_mc.cpython-312.pyc +0 -0
  111. vbagent-0.1.0/vbagent/prompts/scanner/__pycache__/mcq_sc.cpython-312.pyc +0 -0
  112. vbagent-0.1.0/vbagent/prompts/scanner/__pycache__/passage.cpython-312.pyc +0 -0
  113. vbagent-0.1.0/vbagent/prompts/scanner/__pycache__/subjective.cpython-312.pyc +0 -0
  114. vbagent-0.1.0/vbagent/prompts/scanner/assertion_reason.py +64 -0
  115. vbagent-0.1.0/vbagent/prompts/scanner/common.py +216 -0
  116. vbagent-0.1.0/vbagent/prompts/scanner/match.py +94 -0
  117. vbagent-0.1.0/vbagent/prompts/scanner/mcq_mc.py +68 -0
  118. vbagent-0.1.0/vbagent/prompts/scanner/mcq_sc.py +64 -0
  119. vbagent-0.1.0/vbagent/prompts/scanner/passage.py +76 -0
  120. vbagent-0.1.0/vbagent/prompts/scanner/subjective.py +56 -0
  121. vbagent-0.1.0/vbagent/prompts/solution_checker.py +114 -0
  122. vbagent-0.1.0/vbagent/prompts/tikz.py +486 -0
  123. vbagent-0.1.0/vbagent/prompts/tikz_checker.py +230 -0
  124. vbagent-0.1.0/vbagent/prompts/variants/__init__.py +23 -0
  125. vbagent-0.1.0/vbagent/prompts/variants/__pycache__/__init__.cpython-312.pyc +0 -0
  126. vbagent-0.1.0/vbagent/prompts/variants/__pycache__/conceptual.cpython-312.pyc +0 -0
  127. vbagent-0.1.0/vbagent/prompts/variants/__pycache__/conceptual_calculus.cpython-312.pyc +0 -0
  128. vbagent-0.1.0/vbagent/prompts/variants/__pycache__/context.cpython-312.pyc +0 -0
  129. vbagent-0.1.0/vbagent/prompts/variants/__pycache__/multi_context.cpython-312.pyc +0 -0
  130. vbagent-0.1.0/vbagent/prompts/variants/__pycache__/numerical.cpython-312.pyc +0 -0
  131. vbagent-0.1.0/vbagent/prompts/variants/conceptual.py +55 -0
  132. vbagent-0.1.0/vbagent/prompts/variants/conceptual_calculus.py +60 -0
  133. vbagent-0.1.0/vbagent/prompts/variants/context.py +50 -0
  134. vbagent-0.1.0/vbagent/prompts/variants/multi_context.py +62 -0
  135. vbagent-0.1.0/vbagent/prompts/variants/numerical.py +51 -0
  136. vbagent-0.1.0/vbagent/references/__init__.py +5 -0
  137. vbagent-0.1.0/vbagent/references/__pycache__/__init__.cpython-312.pyc +0 -0
  138. vbagent-0.1.0/vbagent/references/__pycache__/context.cpython-312.pyc +0 -0
  139. vbagent-0.1.0/vbagent/references/__pycache__/store.cpython-312.pyc +0 -0
  140. vbagent-0.1.0/vbagent/references/__pycache__/tikz_store.cpython-312.pyc +0 -0
  141. vbagent-0.1.0/vbagent/references/context.py +404 -0
  142. vbagent-0.1.0/vbagent/references/store.py +331 -0
  143. vbagent-0.1.0/vbagent/references/tikz_store.py +509 -0
  144. vbagent-0.1.0/vbagent/templates/__init__.py +5 -0
  145. vbagent-0.1.0/vbagent/templates/__pycache__/__init__.cpython-312.pyc +0 -0
  146. vbagent-0.1.0/vbagent/templates/__pycache__/agentic_context.cpython-312.pyc +0 -0
  147. vbagent-0.1.0/vbagent/templates/agentic_context.py +193 -0
vbagent-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.3
2
+ Name: vbagent
3
+ Version: 0.1.0
4
+ Summary: CLI tool for physics question image scanning, categorization, and variant generation
5
+ Author: vaibhavblayer
6
+ Author-email: vaibhavblayer@gmail.com
7
+ Requires-Python: >=3.12
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Provides-Extra: dev
12
+ Requires-Dist: click (>=8.1.0)
13
+ Requires-Dist: hypothesis (>=6.0.0) ; extra == "dev"
14
+ Requires-Dist: openai-agents (>=0.6.2)
15
+ Requires-Dist: pillow (>=10.0.0)
16
+ Requires-Dist: pydantic (>=2.0.0)
17
+ Requires-Dist: pytest (>=8.0.0) ; extra == "dev"
18
+ Requires-Dist: rich (>=13.0.0)
19
+ Description-Content-Type: text/markdown
20
+
21
+
File without changes
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "vbagent"
3
+ version = "0.1.0"
4
+ description = "CLI tool for physics question image scanning, categorization, and variant generation"
5
+ authors = [
6
+ {name = "vaibhavblayer", email = "vaibhavblayer@gmail.com"}
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "openai-agents>=0.6.2",
12
+ "pydantic>=2.0.0",
13
+ "click>=8.1.0",
14
+ "pillow>=10.0.0",
15
+ "rich>=13.0.0",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ dev = [
20
+ "pytest>=8.0.0",
21
+ "hypothesis>=6.0.0",
22
+ ]
23
+
24
+ [project.scripts]
25
+ vbagent = "vbagent.cli.main:main"
26
+
27
+ [build-system]
28
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
29
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,3 @@
1
+ """VBAgent - Physics question processing pipeline using OpenAI."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,127 @@
1
+ """Agent modules for vbagent using openai-agents SDK.
2
+
3
+ Uses lazy imports to avoid loading heavy dependencies (openai, agents, mcp, pydantic)
4
+ until they are actually needed. This significantly improves CLI startup time.
5
+ """
6
+
7
+ from typing import TYPE_CHECKING
8
+
9
+ # Only import for type checking - avoids heavy runtime imports
10
+ if TYPE_CHECKING:
11
+ from .base import (
12
+ encode_image,
13
+ create_image_message,
14
+ create_agent,
15
+ run_agent,
16
+ run_agent_sync,
17
+ )
18
+ from .classifier import classifier_agent, classify
19
+ from .idea import idea_agent_json, idea_agent_latex, extract_ideas, generate_idea_latex
20
+ from .alternate import alternate_agent, generate_alternate, extract_answer
21
+ from .selector import (
22
+ ProblemContext,
23
+ discover_problems,
24
+ select_random,
25
+ load_problem_context,
26
+ )
27
+ from .solution_checker import (
28
+ solution_checker_agent,
29
+ check_solution,
30
+ has_solution_passed,
31
+ )
32
+ from .grammar_checker import (
33
+ grammar_checker_agent,
34
+ check_grammar,
35
+ has_grammar_passed,
36
+ )
37
+ from .clarity_checker import (
38
+ clarity_checker_agent,
39
+ check_clarity,
40
+ has_clarity_passed,
41
+ )
42
+ from .tikz_checker import (
43
+ tikz_checker_agent,
44
+ check_tikz,
45
+ has_tikz_passed,
46
+ has_tikz_environment,
47
+ )
48
+
49
+
50
+ __all__ = [
51
+ "encode_image",
52
+ "create_image_message",
53
+ "create_agent",
54
+ "run_agent",
55
+ "run_agent_sync",
56
+ "classifier_agent",
57
+ "classify",
58
+ "idea_agent_json",
59
+ "idea_agent_latex",
60
+ "extract_ideas",
61
+ "generate_idea_latex",
62
+ "alternate_agent",
63
+ "generate_alternate",
64
+ "extract_answer",
65
+ "ProblemContext",
66
+ "discover_problems",
67
+ "select_random",
68
+ "load_problem_context",
69
+ # Solution checker
70
+ "solution_checker_agent",
71
+ "check_solution",
72
+ "has_solution_passed",
73
+ # Grammar checker
74
+ "grammar_checker_agent",
75
+ "check_grammar",
76
+ "has_grammar_passed",
77
+ # Clarity checker
78
+ "clarity_checker_agent",
79
+ "check_clarity",
80
+ "has_clarity_passed",
81
+ # TikZ checker
82
+ "tikz_checker_agent",
83
+ "check_tikz",
84
+ "has_tikz_passed",
85
+ "has_tikz_environment",
86
+ ]
87
+
88
+
89
+ def __getattr__(name: str):
90
+ """Lazy import of agent modules to speed up CLI startup."""
91
+ if name in ("encode_image", "create_image_message", "create_agent", "run_agent", "run_agent_sync"):
92
+ from . import base
93
+ return getattr(base, name)
94
+
95
+ if name in ("classifier_agent", "classify"):
96
+ from . import classifier
97
+ return getattr(classifier, name)
98
+
99
+ if name in ("idea_agent_json", "idea_agent_latex", "extract_ideas", "generate_idea_latex"):
100
+ from . import idea
101
+ return getattr(idea, name)
102
+
103
+ if name in ("alternate_agent", "generate_alternate", "extract_answer"):
104
+ from . import alternate
105
+ return getattr(alternate, name)
106
+
107
+ if name in ("ProblemContext", "discover_problems", "select_random", "load_problem_context"):
108
+ from . import selector
109
+ return getattr(selector, name)
110
+
111
+ if name in ("solution_checker_agent", "check_solution", "has_solution_passed"):
112
+ from . import solution_checker
113
+ return getattr(solution_checker, name)
114
+
115
+ if name in ("grammar_checker_agent", "check_grammar", "has_grammar_passed"):
116
+ from . import grammar_checker
117
+ return getattr(grammar_checker, name)
118
+
119
+ if name in ("clarity_checker_agent", "check_clarity", "has_clarity_passed"):
120
+ from . import clarity_checker
121
+ return getattr(clarity_checker, name)
122
+
123
+ if name in ("tikz_checker_agent", "check_tikz", "has_tikz_passed", "has_tikz_environment"):
124
+ from . import tikz_checker
125
+ return getattr(tikz_checker, name)
126
+
127
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,178 @@
1
+ """Alternate solution agent for physics problems.
2
+
3
+ Uses openai-agents SDK to generate alternative solution methods
4
+ while maintaining the same final answer.
5
+ """
6
+
7
+ import re
8
+ from typing import Optional
9
+
10
+ from vbagent.agents.base import create_agent, run_agent_sync
11
+
12
+
13
+ def clean_latex_output(latex: str) -> str:
14
+ """Clean up LaTeX output by removing markdown code block markers.
15
+
16
+ Removes patterns like:
17
+ - ```latex ... ```
18
+ - ``` ... ```
19
+ - Leading/trailing whitespace
20
+
21
+ Args:
22
+ latex: Raw LaTeX output from LLM
23
+
24
+ Returns:
25
+ Cleaned LaTeX without markdown artifacts
26
+ """
27
+ if not latex:
28
+ return latex
29
+
30
+ # Remove markdown code block markers with language specifier
31
+ # Matches: ```latex, ```tex, ```LaTeX, etc.
32
+ latex = re.sub(r'^```(?:latex|tex|LaTeX)?\s*\n?', '', latex, flags=re.IGNORECASE)
33
+
34
+ # Remove closing code block marker
35
+ latex = re.sub(r'\n?```\s*$', '', latex)
36
+
37
+ # Also handle case where ``` appears at the start without newline
38
+ latex = re.sub(r'^```\s*', '', latex)
39
+
40
+ return latex.strip()
41
+
42
+
43
+ from vbagent.models.idea import IdeaResult
44
+ from vbagent.prompts.alternate import (
45
+ SYSTEM_PROMPT,
46
+ USER_TEMPLATE,
47
+ USER_TEMPLATE_WITH_EXISTING,
48
+ )
49
+
50
+
51
+ # Create the alternate solution agent
52
+ alternate_agent = create_agent(
53
+ name="AlternateSolution",
54
+ instructions=SYSTEM_PROMPT,
55
+ agent_type="alternate",
56
+ )
57
+
58
+
59
+ def extract_answer(solution: str) -> str:
60
+ """Extract the final numerical answer from a solution.
61
+
62
+ Looks for common answer patterns in LaTeX solutions.
63
+
64
+ Args:
65
+ solution: The solution text in LaTeX format
66
+
67
+ Returns:
68
+ The extracted answer string, or empty string if not found
69
+ """
70
+ # Common patterns for final answers in physics solutions
71
+ patterns = [
72
+ # Boxed answers: \boxed{...} - handle nested braces
73
+ r'\\boxed\{((?:[^{}]|\{[^{}]*\})*)\}',
74
+ # Answer environment or explicit answer markers
75
+ r'(?:answer|ans|result)\s*[:=]\s*([^\n\\]+)',
76
+ # Final equals in solution: = X (with optional units in \text{})
77
+ r'=\s*([-+]?\d+(?:\.\d+)?(?:\s*\\times\s*10\^?\{?[-+]?\d+\}?)?)\s*(?:\\text\{[^}]*\}|\\[a-zA-Z]+|[a-zA-Z/]+)?(?:\s*$|\s*\\\\|\s*\n)',
78
+ ]
79
+
80
+ for pattern in patterns:
81
+ matches = re.findall(pattern, solution, re.IGNORECASE | re.MULTILINE)
82
+ if matches:
83
+ # Return the last match (usually the final answer)
84
+ return matches[-1].strip()
85
+
86
+ return ""
87
+
88
+
89
+ def extract_existing_alternates(content: str) -> list[str]:
90
+ """Extract existing alternate solutions from LaTeX content.
91
+
92
+ Args:
93
+ content: Full LaTeX content of the problem file
94
+
95
+ Returns:
96
+ List of existing alternate solution contents
97
+ """
98
+ pattern = r'\\begin\{alternatesolution\}(.*?)\\end\{alternatesolution\}'
99
+ matches = re.findall(pattern, content, re.DOTALL)
100
+ return [m.strip() for m in matches]
101
+
102
+
103
+ def has_alternate_solution(content: str) -> bool:
104
+ """Check if content already has an alternate solution.
105
+
106
+ Args:
107
+ content: Full LaTeX content of the problem file
108
+
109
+ Returns:
110
+ True if alternatesolution environment exists
111
+ """
112
+ return r'\begin{alternatesolution}' in content
113
+
114
+
115
+ def count_alternate_solutions(content: str) -> int:
116
+ """Count the number of alternate solutions in content.
117
+
118
+ Args:
119
+ content: Full LaTeX content of the problem file
120
+
121
+ Returns:
122
+ Number of alternatesolution environments
123
+ """
124
+ return content.count(r'\begin{alternatesolution}')
125
+
126
+
127
+ def generate_alternate(
128
+ problem: str,
129
+ solution: str,
130
+ ideas: Optional[IdeaResult] = None,
131
+ existing_alternates: Optional[list[str]] = None,
132
+ full_content: Optional[str] = None,
133
+ ) -> str:
134
+ """Generate an alternate solution approach for a physics problem.
135
+
136
+ Creates a different valid solution method that arrives at the same
137
+ final answer as the original solution.
138
+
139
+ Args:
140
+ problem: The problem statement in LaTeX format (legacy, used if full_content not provided)
141
+ solution: The original solution in LaTeX format (legacy, used if full_content not provided)
142
+ ideas: Optional IdeaResult with extracted concepts and techniques
143
+ existing_alternates: List of existing alternate solutions to avoid repeating
144
+ full_content: Full LaTeX file content (preferred - pass entire file)
145
+
146
+ Returns:
147
+ Alternative solution in LaTeX format within alternatesolution environment
148
+
149
+ Raises:
150
+ ValueError: If content is empty
151
+ """
152
+ # Use full_content if provided, otherwise construct from problem/solution
153
+ if full_content:
154
+ content_to_use = full_content.strip()
155
+ else:
156
+ if not problem.strip():
157
+ raise ValueError("Problem cannot be empty")
158
+ if not solution.strip():
159
+ raise ValueError("Solution cannot be empty")
160
+ content_to_use = f"{problem}\n\n\\begin{{solution}}\n{solution}\n\\end{{solution}}"
161
+
162
+ if not content_to_use:
163
+ raise ValueError("Content cannot be empty")
164
+
165
+ # Check if there are existing alternates in the content
166
+ has_existing = r'\begin{alternatesolution}' in content_to_use
167
+
168
+ # Choose template based on whether existing alternates exist
169
+ # Use string replace instead of .format() to avoid issues with LaTeX curly braces
170
+ if has_existing or existing_alternates:
171
+ message = USER_TEMPLATE_WITH_EXISTING.replace('{full_content}', content_to_use)
172
+ else:
173
+ message = USER_TEMPLATE.replace('{full_content}', content_to_use)
174
+
175
+ raw_result = run_agent_sync(alternate_agent, message)
176
+
177
+ # Clean up markdown artifacts from LLM output
178
+ return clean_latex_output(raw_result)
@@ -0,0 +1,214 @@
1
+ """Base agent utilities using OpenAI Agents SDK."""
2
+
3
+ import base64
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, Any, Optional
6
+
7
+ # Lazy import for heavy agents SDK - only import at runtime when needed
8
+ if TYPE_CHECKING:
9
+ from agents import Agent, ModelSettings
10
+
11
+ from vbagent.config import get_model, get_model_settings
12
+
13
+
14
+ def _get_agent_class():
15
+ """Lazy import of Agent class."""
16
+ from agents import Agent
17
+ return Agent
18
+
19
+
20
+ def _get_runner_class():
21
+ """Lazy import of Runner class."""
22
+ from agents import Runner
23
+ return Runner
24
+
25
+
26
+ def _get_model_settings_class():
27
+ """Lazy import of ModelSettings class."""
28
+ from agents import ModelSettings
29
+ return ModelSettings
30
+
31
+
32
+ def encode_image(image_path: str) -> tuple[str, str]:
33
+ """Encode an image file to base64.
34
+
35
+ Args:
36
+ image_path: Path to the image file
37
+
38
+ Returns:
39
+ Tuple of (base64_data, media_type)
40
+ """
41
+ path = Path(image_path)
42
+ if not path.exists():
43
+ raise FileNotFoundError(f"Image not found: {image_path}")
44
+
45
+ with open(path, "rb") as f:
46
+ image_bytes = f.read()
47
+
48
+ image_data = base64.b64encode(image_bytes).decode("utf-8")
49
+
50
+ suffix = path.suffix.lower()
51
+ media_type_map = {
52
+ ".jpg": "image/jpeg",
53
+ ".jpeg": "image/jpeg",
54
+ ".png": "image/png",
55
+ ".gif": "image/gif",
56
+ ".webp": "image/webp",
57
+ }
58
+ media_type = media_type_map.get(suffix, "image/jpeg")
59
+
60
+ return image_data, media_type
61
+
62
+
63
+ def create_image_message(image_path: str, text: str) -> list[dict[str, Any]]:
64
+ """Create a message with image and text for the agent.
65
+
66
+ Uses the OpenAI Responses API format for image input.
67
+
68
+ Args:
69
+ image_path: Path to the image file
70
+ text: Text message to accompany the image
71
+
72
+ Returns:
73
+ List containing a single message dict in Responses API format
74
+ """
75
+ image_data, media_type = encode_image(image_path)
76
+ # Responses API format: message with content list containing input_image and input_text
77
+ return [
78
+ {
79
+ "type": "message",
80
+ "role": "user",
81
+ "content": [
82
+ {
83
+ "type": "input_image",
84
+ "image_url": f"data:{media_type};base64,{image_data}",
85
+ "detail": "auto"
86
+ },
87
+ {"type": "input_text", "text": text}
88
+ ]
89
+ }
90
+ ]
91
+
92
+
93
+ def create_agent(
94
+ name: str,
95
+ instructions: str,
96
+ model: Optional[str] = None,
97
+ model_settings: Optional["ModelSettings"] = None,
98
+ output_type: Optional[type] = None,
99
+ tools: Optional[list] = None,
100
+ agent_type: Optional[str] = None,
101
+ ) -> "Agent":
102
+ """Create an agent with default configuration.
103
+
104
+ Args:
105
+ name: Agent name
106
+ instructions: System prompt / instructions
107
+ model: Model to use (if None, uses config for agent_type)
108
+ model_settings: Optional ModelSettings for temperature, max_tokens, etc.
109
+ output_type: Optional Pydantic model for structured output
110
+ tools: Optional list of tools (@function_tool decorated functions)
111
+ agent_type: Agent type for config lookup (classifier, scanner, tikz, etc.)
112
+
113
+ Returns:
114
+ Configured Agent instance
115
+ """
116
+ Agent = _get_agent_class()
117
+
118
+ # Get model and settings from config if not explicitly provided
119
+ if model is None:
120
+ model = get_model(agent_type or "default")
121
+ if model_settings is None:
122
+ model_settings = get_model_settings(agent_type or "default")
123
+
124
+ return Agent(
125
+ name=name,
126
+ instructions=instructions,
127
+ model=model,
128
+ model_settings=model_settings,
129
+ output_type=output_type,
130
+ tools=tools or [],
131
+ )
132
+
133
+
134
+ def _print_agent_info(agent: "Agent") -> None:
135
+ """Print agent information when running.
136
+
137
+ Args:
138
+ agent: The Agent instance being run
139
+ """
140
+ model = agent.model or "default"
141
+ reasoning = "unknown"
142
+
143
+ # Extract reasoning effort from model_settings if available
144
+ if agent.model_settings:
145
+ settings = agent.model_settings
146
+ if hasattr(settings, 'reasoning') and settings.reasoning:
147
+ reasoning_obj = settings.reasoning
148
+ # Handle both dict and Reasoning object
149
+ if isinstance(reasoning_obj, dict):
150
+ reasoning = reasoning_obj.get('effort', 'unknown')
151
+ elif hasattr(reasoning_obj, 'effort'):
152
+ reasoning = reasoning_obj.effort or 'unknown'
153
+
154
+ # Use dim styling for subtle output
155
+ print(f"\033[2m⚡ {agent.name} | model: {model} | reasoning: {reasoning}\033[0m")
156
+
157
+
158
+ async def run_agent(agent: "Agent", input_text: str | list) -> Any:
159
+ """Run an agent asynchronously and return the final output.
160
+
161
+ Args:
162
+ agent: The Agent instance to run
163
+ input_text: The input text or message (can be string or list for images)
164
+
165
+ Returns:
166
+ The agent's final output (string or structured type)
167
+ """
168
+ Runner = _get_runner_class()
169
+ _print_agent_info(agent)
170
+ result = await Runner.run(agent, input=input_text)
171
+ return result.final_output
172
+
173
+
174
+ def run_agent_sync(agent: "Agent", input_text: str | list) -> Any:
175
+ """Run an agent synchronously and return the final output.
176
+
177
+ Uses a thread to allow immediate Ctrl+C interruption.
178
+
179
+ Args:
180
+ agent: The Agent instance to run
181
+ input_text: The input text or message (can be string or list for images)
182
+
183
+ Returns:
184
+ The agent's final output (string or structured type)
185
+
186
+ Raises:
187
+ KeyboardInterrupt: If user presses Ctrl+C
188
+ """
189
+ import concurrent.futures
190
+ import threading
191
+
192
+ Runner = _get_runner_class()
193
+ _print_agent_info(agent)
194
+
195
+ # Use a thread pool to run the agent, allowing Ctrl+C to interrupt
196
+ result_holder = {"result": None, "error": None}
197
+
198
+ def run_in_thread():
199
+ try:
200
+ result_holder["result"] = Runner.run_sync(agent, input=input_text)
201
+ except Exception as e:
202
+ result_holder["error"] = e
203
+
204
+ thread = threading.Thread(target=run_in_thread, daemon=True)
205
+ thread.start()
206
+
207
+ # Wait for thread with small intervals to allow Ctrl+C handling
208
+ while thread.is_alive():
209
+ thread.join(timeout=0.1) # Check every 100ms for interrupt
210
+
211
+ if result_holder["error"]:
212
+ raise result_holder["error"]
213
+
214
+ return result_holder["result"].final_output