anouar-bm commited on
Commit
498af49
·
1 Parent(s): cbae203
.env.example ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # API Keys for LLM-based optimization
2
+ # Get your AIMLAPI key from: https://aimlapi.com
3
+ AIMLAPI_API_KEY=your_aimlapi_key_here
4
+
5
+ # Get your Tavily API key from: https://tavily.com
6
+ TAVILY_API_KEY=your_tavily_key_here
7
+
8
+ LANGSMITH_TRACING="true"
9
+ LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
10
+ LANGSMITH_API_KEY=""
11
+ LANGSMITH_PROJECT=""
12
+
Dockerfile CHANGED
@@ -17,4 +17,4 @@ EXPOSE 8501
17
 
18
  HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
19
 
20
- ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
17
 
18
  HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
19
 
20
+ ENTRYPOINT ["streamlit", "run", "src/app.py", "--server.port=8501", "--server.address=0.0.0.0"]
README.md CHANGED
@@ -1,19 +1,70 @@
1
  ---
2
- title: McpOptimizer
3
  emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
  sdk: docker
7
  app_port: 8501
8
  tags:
9
  - streamlit
 
 
 
 
10
  pinned: false
11
- short_description: Streamlit template space
 
12
  ---
13
 
14
- # Welcome to Streamlit!
15
 
16
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
17
 
18
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: AI Prompt Optimizer
3
  emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
  app_port: 8501
8
  tags:
9
  - streamlit
10
+ - ai
11
+ - llm
12
+ - optimization
13
+ - prompt-engineering
14
  pinned: false
15
+ short_description: Advanced AI prompt optimizer with cost reduction and LLM integration
16
+ license: mit
17
  ---
18
 
19
+ # AI Prompt Optimizer
20
 
21
+ An advanced Streamlit web application that helps reduce the cost of using large language models (LLMs) by intelligently optimizing prompts. The app shortens prompts by removing filler words, simplifying phrases, and using sophisticated linguistic techniques, thereby reducing the number of tokens sent to APIs.
22
 
23
+ ## Features
24
+
25
+ - **Advanced Prompt Optimization**: Rule-based optimization using spaCy and linguistic techniques
26
+ - **LLM-based Optimization**: 8 specialized personas for context-aware optimization
27
+ - **Real-time Cost Analysis**: Accurate token counting and cost savings calculation
28
+ - **Multi-Model Support**: GPT-4, GPT-5, Claude, LLaMA 2, and custom models
29
+ - **Professional UI**: Modern, gradient-based interface with responsive design
30
+ - **Tavily Search Integration**: Enhanced prompts with real-time web information
31
+ - **MCP Server Support**: Model Context Protocol integration for advanced workflows
32
+
33
+ ## Technologies Used
34
+
35
+ - **Streamlit**: Modern web interface
36
+ - **spaCy**: Natural language processing
37
+ - **tiktoken**: Accurate token counting
38
+ - **LangChain**: LLM integration and metadata tracking
39
+ - **Tavily**: Web search API integration
40
+
41
+ ## Usage
42
+
43
+ 1. Enter your prompt text
44
+ 2. Choose optimization method (Local or LLM-based)
45
+ 3. Select model and persona (for LLM optimization)
46
+ 4. Click "Optimize" to see results and cost savings
47
+
48
+ Perfect for developers, content creators, and AI enthusiasts looking to reduce LLM costs while maintaining prompt effectiveness.
49
+
50
+ ## Deployment on Hugging Face Spaces
51
+
52
+ This app is ready to deploy on Hugging Face Spaces. To deploy:
53
+
54
+ 1. **Fork this repository** or upload the files to your Hugging Face Space
55
+ 2. **Set up environment variables** in your Space settings:
56
+ - `AIMLAPI_API_KEY`: Get from [AIMLAPI](https://aimlapi.com) (required for LLM-based optimization)
57
+ - `TAVILY_API_KEY`: Get from [Tavily](https://tavily.com) (required for agent functionality)
58
+ 3. **Configure your Space**:
59
+ - SDK: Docker
60
+ - App port: 8501
61
+ - Hardware: CPU basic (recommended)
62
+
63
+ The app will automatically detect missing API keys and allow users to enter them manually if needed.
64
+
65
+ ## Local Development
66
+
67
+ 1. Clone the repository
68
+ 2. Install dependencies: `pip install -r requirements.txt`
69
+ 3. Copy `.env.example` to `.env` and add your API keys
70
+ 4. Run: `streamlit run src/app.py`
requirements.txt CHANGED
@@ -1,3 +1,138 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiohappyeyeballs==2.6.1
2
+ aiohttp==3.12.15
3
+ aiosignal==1.4.0
4
+ altair==5.5.0
5
+ annotated-types==0.7.0
6
+ anyio==4.10.0
7
+ attrs==25.3.0
8
+ authlib==1.6.2
9
+ blinker==1.9.0
10
+ blis==0.7.11
11
+ cachetools==6.1.0
12
+ catalogue==2.0.10
13
+ certifi==2025.8.3
14
+ cffi==1.17.1
15
+ charset-normalizer==3.4.3
16
+ click==8.2.1
17
+ cloudpathlib==0.21.1
18
+ confection==0.1.5
19
+ cryptography==45.0.6
20
+ cyclopts==3.22.5
21
+ cymem==2.0.11
22
+ distro==1.9.0
23
+ dnspython==2.7.0
24
+ docstring-parser==0.17.0
25
+ docutils==0.22
26
+ email-validator==2.2.0
27
+ en-core-web-sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.7.0/en_core_web_sm-3.7.0.tar.gz
28
+ exceptiongroup==1.3.0
29
+ fastmcp==2.11.3
30
+ frozenlist==1.7.0
31
+ gitdb==4.0.12
32
+ gitpython==3.1.45
33
+ h11==0.16.0
34
+ httpcore==1.0.9
35
+ httpx==0.28.1
36
+ httpx-sse==0.4.1
37
+ idna==3.10
38
+ isodate==0.7.2
39
+ jinja2==3.1.6
40
+ jiter==0.10.0
41
+ jsonpatch==1.33
42
+ jsonpointer==3.0.0
43
+ jsonschema==4.25.1
44
+ jsonschema-path==0.3.4
45
+ jsonschema-specifications==2025.4.1
46
+ langchain==0.3.27
47
+ langchain-core==0.3.74
48
+ langchain-openai==0.3.31
49
+ langchain-tavily==0.2.11
50
+ langchain-text-splitters==0.3.9
51
+ langcodes==3.5.0
52
+ langgraph==0.6.6
53
+ langgraph-checkpoint==2.1.1
54
+ langgraph-prebuilt==0.6.4
55
+ langgraph-sdk==0.2.3
56
+ langsmith==0.4.16
57
+ language-data==1.3.0
58
+ lazy-object-proxy==1.12.0
59
+ lemminflect==0.2.3
60
+ marisa-trie==1.3.0
61
+ markdown-it-py==4.0.0
62
+ markupsafe==3.0.2
63
+ mcp==1.13.1
64
+ mdurl==0.1.2
65
+ more-itertools==10.7.0
66
+ multidict==6.6.4
67
+ murmurhash==1.0.13
68
+ narwhals==2.1.2
69
+ numpy==1.26.4
70
+ openai==1.101.0
71
+ openapi-core==0.19.5
72
+ openapi-pydantic==0.5.1
73
+ openapi-schema-validator==0.6.3
74
+ openapi-spec-validator==0.7.2
75
+ orjson==3.11.2
76
+ ormsgpack==1.10.0
77
+ packaging==25.0
78
+ pandas==2.3.2
79
+ parse==1.20.2
80
+ pathable==0.4.4
81
+ pillow==11.3.0
82
+ preshed==3.0.10
83
+ propcache==0.3.2
84
+ protobuf==6.32.0
85
+ pyarrow==21.0.0
86
+ pycparser==2.22
87
+ pydantic==2.11.7
88
+ pydantic-core==2.33.2
89
+ pydantic-settings==2.10.1
90
+ pydeck==0.9.1
91
+ pygments==2.19.2
92
+ pyperclip==1.9.0
93
+ python-dateutil==2.9.0.post0
94
+ python-dotenv==1.1.1
95
+ python-multipart==0.0.20
96
+ pytz==2025.2
97
+ pyyaml==6.0.2
98
+ referencing==0.36.2
99
+ regex==2025.7.34
100
+ requests==2.32.5
101
+ requests-toolbelt==1.0.0
102
+ rfc3339-validator==0.1.4
103
+ rich==14.1.0
104
+ rich-rst==1.3.1
105
+ rpds-py==0.27.0
106
+ setuptools==80.9.0
107
+ shellingham==1.5.4
108
+ six==1.17.0
109
+ smart-open==7.3.0.post1
110
+ smmap==5.0.2
111
+ sniffio==1.3.1
112
+ spacy==3.7.5
113
+ spacy-legacy==3.0.12
114
+ spacy-loggers==1.0.5
115
+ sqlalchemy==2.0.43
116
+ srsly==2.5.1
117
+ sse-starlette==3.0.2
118
+ starlette==0.47.2
119
+ streamlit==1.48.1
120
+ tenacity==9.1.2
121
+ thinc==8.2.5
122
+ tiktoken==0.11.0
123
+ toml==0.10.2
124
+ tornado==6.5.2
125
+ tqdm==4.67.1
126
+ typer==0.16.1
127
+ typing-extensions==4.14.1
128
+ typing-inspection==0.4.1
129
+ tzdata==2025.2
130
+ urllib3==2.5.0
131
+ uvicorn==0.35.0
132
+ wasabi==1.1.3
133
+ weasel==0.4.1
134
+ werkzeug==3.1.1
135
+ wrapt==1.17.3
136
+ xxhash==3.5.0
137
+ yarl==1.20.1
138
+ zstandard==0.24.0
src/.github/workflows/gemini-cli.yml ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: '💬 Gemini CLI'
2
+
3
+ on:
4
+ pull_request_review_comment:
5
+ types:
6
+ - 'created'
7
+ pull_request_review:
8
+ types:
9
+ - 'submitted'
10
+ issue_comment:
11
+ types:
12
+ - 'created'
13
+
14
+ concurrency:
15
+ group: '${{ github.workflow }}-${{ github.event.issue.number }}'
16
+ cancel-in-progress: |-
17
+ ${{ github.event.sender.type == 'User' && ( github.event.issue.author_association == 'OWNER' || github.event.issue.author_association == 'MEMBER' || github.event.issue.author_association == 'COLLABORATOR') }}
18
+
19
+ defaults:
20
+ run:
21
+ shell: 'bash'
22
+
23
+ permissions:
24
+ contents: 'write'
25
+ id-token: 'write'
26
+ pull-requests: 'write'
27
+ issues: 'write'
28
+
29
+ jobs:
30
+ gemini-cli:
31
+ # This condition seeks to ensure the action is only run when it is triggered by a trusted user.
32
+ # For private repos, users who have access to the repo are considered trusted.
33
+ # For public repos, users who members, owners, or collaborators are considered trusted.
34
+ if: |-
35
+ github.event_name == 'workflow_dispatch' ||
36
+ (
37
+ github.event_name == 'issues' && github.event.action == 'opened' &&
38
+ contains(github.event.issue.body, '@gemini-cli') &&
39
+ !contains(github.event.issue.body, '@gemini-cli /review') &&
40
+ !contains(github.event.issue.body, '@gemini-cli /triage') &&
41
+ (
42
+ github.event.repository.private == true ||
43
+ contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.issue.author_association)
44
+ )
45
+ ) ||
46
+ (
47
+ (
48
+ github.event_name == 'issue_comment' ||
49
+ github.event_name == 'pull_request_review_comment'
50
+ ) &&
51
+ contains(github.event.comment.body, '@gemini-cli') &&
52
+ !contains(github.event.comment.body, '@gemini-cli /review') &&
53
+ !contains(github.event.comment.body, '@gemini-cli /triage') &&
54
+ (
55
+ github.event.repository.private == true ||
56
+ contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
57
+ )
58
+ ) ||
59
+ (
60
+ github.event_name == 'pull_request_review' &&
61
+ contains(github.event.review.body, '@gemini-cli') &&
62
+ !contains(github.event.review.body, '@gemini-cli /review') &&
63
+ !contains(github.event.review.body, '@gemini-cli /triage') &&
64
+ (
65
+ github.event.repository.private == true ||
66
+ contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)
67
+ )
68
+ )
69
+ timeout-minutes: 10
70
+ runs-on: 'ubuntu-latest'
71
+ steps:
72
+ - name: 'Generate GitHub App Token'
73
+ id: 'generate_token'
74
+ if: |-
75
+ ${{ vars.APP_ID }}
76
+ uses: 'actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e' # ratchet:actions/create-github-app-token@v2
77
+ with:
78
+ app-id: '${{ vars.APP_ID }}'
79
+ private-key: '${{ secrets.APP_PRIVATE_KEY }}'
80
+
81
+ - name: 'Get context from event'
82
+ id: 'get_context'
83
+ env:
84
+ EVENT_NAME: '${{ github.event_name }}'
85
+ EVENT_PAYLOAD: '${{ toJSON(github.event) }}'
86
+ run: |-
87
+ set -euo pipefail
88
+
89
+ USER_REQUEST=""
90
+ ISSUE_NUMBER=""
91
+ IS_PR="false"
92
+
93
+ if [[ "${EVENT_NAME}" == "issues" ]]; then
94
+ USER_REQUEST=$(echo "${EVENT_PAYLOAD}" | jq -r .issue.body)
95
+ ISSUE_NUMBER=$(echo "${EVENT_PAYLOAD}" | jq -r .issue.number)
96
+ elif [[ "${EVENT_NAME}" == "issue_comment" ]]; then
97
+ USER_REQUEST=$(echo "${EVENT_PAYLOAD}" | jq -r .comment.body)
98
+ ISSUE_NUMBER=$(echo "${EVENT_PAYLOAD}" | jq -r .issue.number)
99
+ if [[ $(echo "${EVENT_PAYLOAD}" | jq -r .issue.pull_request) != "null" ]]; then
100
+ IS_PR="true"
101
+ fi
102
+ elif [[ "${EVENT_NAME}" == "pull_request_review" ]]; then
103
+ USER_REQUEST=$(echo "${EVENT_PAYLOAD}" | jq -r .review.body)
104
+ ISSUE_NUMBER=$(echo "${EVENT_PAYLOAD}" | jq -r .pull_request.number)
105
+ IS_PR="true"
106
+ elif [[ "${EVENT_NAME}" == "pull_request_review_comment" ]]; then
107
+ USER_REQUEST=$(echo "${EVENT_PAYLOAD}" | jq -r .comment.body)
108
+ ISSUE_NUMBER=$(echo "${EVENT_PAYLOAD}" | jq -r .pull_request.number)
109
+ IS_PR="true"
110
+ fi
111
+
112
+ # Clean up user request
113
+ USER_REQUEST=$(echo "${USER_REQUEST}" | sed 's/.*@gemini-cli//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
114
+
115
+ {
116
+ echo "user_request=${USER_REQUEST}"
117
+ echo "issue_number=${ISSUE_NUMBER}"
118
+ echo "is_pr=${IS_PR}"
119
+ } >> "${GITHUB_OUTPUT}"
120
+
121
+ - name: 'Set up git user for commits'
122
+ run: |-
123
+ git config --global user.name 'gemini-cli[bot]'
124
+ git config --global user.email 'gemini-cli[bot]@users.noreply.github.com'
125
+
126
+ - name: 'Checkout PR branch'
127
+ if: |-
128
+ ${{ steps.get_context.outputs.is_pr == 'true' }}
129
+ uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
130
+ with:
131
+ token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
132
+ repository: '${{ github.repository }}'
133
+ ref: 'refs/pull/${{ steps.get_context.outputs.issue_number }}/head'
134
+ fetch-depth: 0
135
+
136
+ - name: 'Checkout main branch'
137
+ if: |-
138
+ ${{ steps.get_context.outputs.is_pr == 'false' }}
139
+ uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
140
+ with:
141
+ token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
142
+ repository: '${{ github.repository }}'
143
+ fetch-depth: 0
144
+
145
+ - name: 'Acknowledge request'
146
+ env:
147
+ GITHUB_ACTOR: '${{ github.actor }}'
148
+ GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
149
+ ISSUE_NUMBER: '${{ steps.get_context.outputs.issue_number }}'
150
+ REPOSITORY: '${{ github.repository }}'
151
+ REQUEST_TYPE: '${{ steps.get_context.outputs.request_type }}'
152
+ run: |-
153
+ set -euo pipefail
154
+ MESSAGE="@${GITHUB_ACTOR} I've received your request and I'm working on it now! 🤖"
155
+ if [[ -n "${MESSAGE}" ]]; then
156
+ gh issue comment "${ISSUE_NUMBER}" \
157
+ --body "${MESSAGE}" \
158
+ --repo "${REPOSITORY}"
159
+ fi
160
+
161
+ - name: 'Get description'
162
+ id: 'get_description'
163
+ env:
164
+ GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
165
+ IS_PR: '${{ steps.get_context.outputs.is_pr }}'
166
+ ISSUE_NUMBER: '${{ steps.get_context.outputs.issue_number }}'
167
+ run: |-
168
+ set -euo pipefail
169
+ if [[ "${IS_PR}" == "true" ]]; then
170
+ DESCRIPTION=$(gh pr view "${ISSUE_NUMBER}" --json body --template '{{.body}}')
171
+ else
172
+ DESCRIPTION=$(gh issue view "${ISSUE_NUMBER}" --json body --template '{{.body}}')
173
+ fi
174
+ {
175
+ echo "description<<EOF"
176
+ echo "${DESCRIPTION}"
177
+ echo "EOF"
178
+ } >> "${GITHUB_OUTPUT}"
179
+
180
+ - name: 'Get comments'
181
+ id: 'get_comments'
182
+ env:
183
+ GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
184
+ IS_PR: '${{ steps.get_context.outputs.is_pr }}'
185
+ ISSUE_NUMBER: '${{ steps.get_context.outputs.issue_number }}'
186
+ run: |-
187
+ set -euo pipefail
188
+ if [[ "${IS_PR}" == "true" ]]; then
189
+ COMMENTS=$(gh pr view "${ISSUE_NUMBER}" --json comments --template '{{range .comments}}{{.author.login}}: {{.body}}{{"\n"}}{{end}}')
190
+ else
191
+ COMMENTS=$(gh issue view "${ISSUE_NUMBER}" --json comments --template '{{range .comments}}{{.author.login}}: {{.body}}{{"\n"}}{{end}}')
192
+ fi
193
+ {
194
+ echo "comments<<EOF"
195
+ echo "${COMMENTS}"
196
+ echo "EOF"
197
+ } >> "${GITHUB_OUTPUT}"
198
+
199
+ - name: 'Run Gemini'
200
+ id: 'run_gemini'
201
+ uses: 'google-github-actions/run-gemini-cli@v0'
202
+ env:
203
+ GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
204
+ REPOSITORY: '${{ github.repository }}'
205
+ USER_REQUEST: '${{ steps.get_context.outputs.user_request }}'
206
+ ISSUE_NUMBER: '${{ steps.get_context.outputs.issue_number }}'
207
+ IS_PR: '${{ steps.get_context.outputs.is_pr }}'
208
+ with:
209
+ gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
210
+ gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
211
+ gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
212
+ gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
213
+ gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
214
+ use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
215
+ use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
216
+ settings: |-
217
+ {
218
+ "debug": ${{ fromJSON(env.DEBUG || env.ACTIONS_STEP_DEBUG || false) }},
219
+ "maxSessionTurns": 50,
220
+ "telemetry": {
221
+ "enabled": false,
222
+ "target": "gcp"
223
+ }
224
+ }
225
+ prompt: |-
226
+ ## Role
227
+
228
+ You are a helpful AI assistant invoked via a CLI interface in a GitHub workflow. You have access to tools to interact with the repository and respond to the user.
229
+
230
+ ## Context
231
+
232
+ - **Repository**: `${{ github.repository }}`
233
+ - **Triggering Event**: `${{ github.event_name }}`
234
+ - **Issue/PR Number**: `${{ steps.get_context.outputs.issue_number }}`
235
+ - **Is this a PR?**: `${{ steps.get_context.outputs.is_pr }}`
236
+ - **Issue/PR Description**:
237
+ `${{ steps.get_description.outputs.description }}`
238
+ - **Comments**:
239
+ `${{ steps.get_comments.outputs.comments }}`
240
+
241
+ ## User Request
242
+
243
+ The user has sent the following request:
244
+ `${{ steps.get_context.outputs.user_request }}`
245
+
246
+ ## How to Respond to Issues, PR Comments, and Questions
247
+
248
+ This workflow supports three main scenarios:
249
+
250
+ 1. **Creating a Fix for an Issue**
251
+ - Carefully read the user request and the related issue or PR description.
252
+ - Use available tools to gather all relevant context (e.g., `gh issue view`, `gh pr view`, `gh pr diff`, `cat`, `head`, `tail`).
253
+ - Identify the root cause of the problem before proceeding.
254
+ - **Show and maintain a plan as a checklist**:
255
+ - At the very beginning, outline the steps needed to resolve the issue or address the request and post them as a checklist comment on the issue or PR (use GitHub markdown checkboxes: `- [ ] Task`).
256
+ - Example:
257
+ ```
258
+ ### Plan
259
+ - [ ] Investigate the root cause
260
+ - [ ] Implement the fix in `file.py`
261
+ - [ ] Add/modify tests
262
+ - [ ] Update documentation
263
+ - [ ] Verify the fix and close the issue
264
+ ```
265
+ - Use: `gh pr comment "${ISSUE_NUMBER}" --body "<plan>"` or `gh issue comment "${ISSUE_NUMBER}" --body "<plan>"` to post the initial plan.
266
+ - As you make progress, keep the checklist visible and up to date by editing the same comment (check off completed tasks with `- [x]`).
267
+ - To update the checklist:
268
+ 1. Find the comment ID for the checklist (use `gh pr comment list "${ISSUE_NUMBER}"` or `gh issue comment list "${ISSUE_NUMBER}"`).
269
+ 2. Edit the comment with the updated checklist:
270
+ - For PRs: `gh pr comment --edit <comment-id> --body "<updated plan>"`
271
+ - For Issues: `gh issue comment --edit <comment-id> --body "<updated plan>"`
272
+ 3. The checklist should only be maintained as a comment on the issue or PR. Do not track or update the checklist in code files.
273
+ - If the fix requires code changes, determine which files and lines are affected. If clarification is needed, note any questions for the user.
274
+ - Make the necessary code or documentation changes using the available tools (e.g., `write_file`). Ensure all changes follow project conventions and best practices. Reference all shell variables as `"${VAR}"` (with quotes and braces) to prevent errors.
275
+ - Run any relevant tests or checks to verify the fix works as intended. If possible, provide evidence (test output, screenshots, etc.) that the issue is resolved.
276
+ - **Branching and Committing**:
277
+ - **NEVER commit directly to the `main` branch.**
278
+ - If you are working on a **pull request** (`IS_PR` is `true`), the correct branch is already checked out. Simply commit and push to it.
279
+ - `git add .`
280
+ - `git commit -m "feat: <describe the change>"`
281
+ - `git push`
282
+ - If you are working on an **issue** (`IS_PR` is `false`), create a new branch for your changes. A good branch name would be `issue/${ISSUE_NUMBER}/<short-description>`.
283
+ - `git checkout -b issue/${ISSUE_NUMBER}/my-fix`
284
+ - `git add .`
285
+ - `git commit -m "feat: <describe the fix>"`
286
+ - `git push origin issue/${ISSUE_NUMBER}/my-fix`
287
+ - After pushing, you can create a pull request: `gh pr create --title "Fixes #${ISSUE_NUMBER}: <short title>" --body "This PR addresses issue #${ISSUE_NUMBER}."`
288
+ - Summarize what was changed and why in a markdown file: `write_file("response.md", "<your response here>")`
289
+ - Post the response as a comment:
290
+ - For PRs: `gh pr comment "${ISSUE_NUMBER}" --body-file response.md`
291
+ - For Issues: `gh issue comment "${ISSUE_NUMBER}" --body-file response.md`
292
+
293
+ 2. **Addressing Comments on a Pull Request**
294
+ - Read the specific comment and the context of the PR.
295
+ - Use tools like `gh pr view`, `gh pr diff`, and `cat` to understand the code and discussion.
296
+ - If the comment requests a change or clarification, follow the same process as for fixing an issue: create a checklist plan, implement, test, and commit any required changes, updating the checklist as you go.
297
+ - **Committing Changes**: The correct PR branch is already checked out. Simply add, commit, and push your changes.
298
+ - `git add .`
299
+ - `git commit -m "fix: address review comments"`
300
+ - `git push`
301
+ - If the comment is a question, answer it directly and clearly, referencing code or documentation as needed.
302
+ - Document your response in `response.md` and post it as a PR comment: `gh pr comment "${ISSUE_NUMBER}" --body-file response.md`
303
+
304
+ 3. **Answering Any Question on an Issue**
305
+ - Read the question and the full issue context using `gh issue view` and related tools.
306
+ - Research or analyze the codebase as needed to provide an accurate answer.
307
+ - If the question requires code or documentation changes, follow the fix process above, including creating and updating a checklist plan and **creating a new branch for your changes as described in section 1.**
308
+ - Write a clear, concise answer in `response.md` and post it as an issue comment: `gh issue comment "${ISSUE_NUMBER}" --body-file response.md`
309
+
310
+ ## Guidelines
311
+
312
+ - **Be concise and actionable.** Focus on solving the user's problem efficiently.
313
+ - **Always commit and push your changes if you modify code or documentation.**
314
+ - **If you are unsure about the fix or answer, explain your reasoning and ask clarifying questions.**
315
+ - **Follow project conventions and best practices.**
src/.github/workflows/gemini-issue-automated-triage.yml ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: '🏷️ Gemini Automated Issue Triage'
2
+
3
+ on:
4
+ issues:
5
+ types:
6
+ - 'opened'
7
+ - 'reopened'
8
+ issue_comment:
9
+ types:
10
+ - 'created'
11
+ workflow_dispatch:
12
+ inputs:
13
+ issue_number:
14
+ description: 'issue number to triage'
15
+ required: true
16
+ type: 'number'
17
+
18
+ concurrency:
19
+ group: '${{ github.workflow }}-${{ github.event.issue.number }}'
20
+ cancel-in-progress: true
21
+
22
+ defaults:
23
+ run:
24
+ shell: 'bash'
25
+
26
+ permissions:
27
+ contents: 'read'
28
+ id-token: 'write'
29
+ issues: 'write'
30
+ statuses: 'write'
31
+
32
+ jobs:
33
+ triage-issue:
34
+ if: |-
35
+ github.event_name == 'issues' ||
36
+ github.event_name == 'workflow_dispatch' ||
37
+ (
38
+ github.event_name == 'issue_comment' &&
39
+ contains(github.event.comment.body, '@gemini-cli /triage') &&
40
+ contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
41
+ )
42
+ timeout-minutes: 5
43
+ runs-on: 'ubuntu-latest'
44
+ steps:
45
+ - name: 'Checkout repository'
46
+ uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
47
+
48
+ - name: 'Generate GitHub App Token'
49
+ id: 'generate_token'
50
+ if: |-
51
+ ${{ vars.APP_ID }}
52
+ uses: 'actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e' # ratchet:actions/create-github-app-token@v2
53
+ with:
54
+ app-id: '${{ vars.APP_ID }}'
55
+ private-key: '${{ secrets.APP_PRIVATE_KEY }}'
56
+
57
+ - name: 'Get Repository Labels'
58
+ id: 'get_labels'
59
+ uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
60
+ with:
61
+ github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
62
+ script: |-
63
+ const { data: labels } = await github.rest.issues.listLabelsForRepo({
64
+ owner: context.repo.owner,
65
+ repo: context.repo.repo,
66
+ });
67
+ const labelNames = labels.map(label => label.name);
68
+ core.setOutput('available_labels', labelNames.join(','));
69
+ core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`);
70
+ return labelNames;
71
+
72
+ - name: 'Run Gemini Issue Analysis'
73
+ uses: 'google-github-actions/run-gemini-cli@v0'
74
+ id: 'gemini_issue_analysis'
75
+ env:
76
+ GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs
77
+ ISSUE_TITLE: '${{ github.event.issue.title }}'
78
+ ISSUE_BODY: '${{ github.event.issue.body }}'
79
+ ISSUE_NUMBER: '${{ github.event.issue.number }}'
80
+ REPOSITORY: '${{ github.repository }}'
81
+ AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
82
+ with:
83
+ gemini_cli_version: '${{ vars.GEMINI_CLI_VERSION }}'
84
+ gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
85
+ gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
86
+ gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
87
+ gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
88
+ gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
89
+ use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
90
+ use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
91
+ settings: |-
92
+ {
93
+ "debug": ${{ fromJSON(env.DEBUG || env.ACTIONS_STEP_DEBUG || false) }},
94
+ "maxSessionTurns": 25,
95
+ "coreTools": [
96
+ "run_shell_command(echo)"
97
+ ],
98
+ "telemetry": {
99
+ "enabled": false,
100
+ "target": "gcp"
101
+ }
102
+ }
103
+ prompt: |-
104
+ ## Role
105
+
106
+ You are an issue triage assistant. Analyze the current GitHub issue
107
+ and identify the most appropriate existing labels. Use the available
108
+ tools to gather information; do not ask for information to be
109
+ provided.
110
+
111
+ ## Steps
112
+
113
+ 1. Review the available labels in the environment variable: "${AVAILABLE_LABELS}".
114
+ 2. Review the issue title and body provided in the environment
115
+ variables: "${ISSUE_TITLE}" and "${ISSUE_BODY}".
116
+ 3. Classify the issue by the appropriate labels from the available labels.
117
+ 4. Output the appropriate labels for this issue in JSON format with explanation, for example:
118
+ ```
119
+ {"labels_to_set": ["kind/bug", "priority/p0"], "explanation": "This is a critical bug report affecting main functionality"}
120
+ ```
121
+ 5. If the issue cannot be classified using the available labels, output:
122
+ ```
123
+ {"labels_to_set": [], "explanation": "Unable to classify this issue with available labels"}
124
+ ```
125
+
126
+ ## Guidelines
127
+
128
+ - Only use labels that already exist in the repository
129
+ - Assign all applicable labels based on the issue content
130
+ - Reference all shell variables as "${VAR}" (with quotes and braces)
131
+ - Output only valid JSON format
132
+ - Do not include any explanation or additional text, just the JSON
133
+
134
+ - name: 'Apply Labels to Issue'
135
+ if: |-
136
+ ${{ steps.gemini_issue_analysis.outputs.summary != '' }}
137
+ env:
138
+ REPOSITORY: '${{ github.repository }}'
139
+ ISSUE_NUMBER: '${{ github.event.issue.number }}'
140
+ LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}'
141
+ uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
142
+ with:
143
+ github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
144
+ script: |-
145
+ // Strip code block markers if present
146
+ const rawLabels = process.env.LABELS_OUTPUT;
147
+ core.info(`Raw labels JSON: ${rawLabels}`);
148
+ let parsedLabels;
149
+ try {
150
+ const trimmedLabels = rawLabels.replace(/^```(?:json)?\s*/, '').replace(/\s*```$/, '').trim();
151
+ parsedLabels = JSON.parse(trimmedLabels);
152
+ core.info(`Parsed labels JSON: ${JSON.stringify(parsedLabels)}`);
153
+ } catch (err) {
154
+ core.setFailed(`Failed to parse labels JSON from Gemini output: ${err.message}\nRaw output: ${rawLabels}`);
155
+ return;
156
+ }
157
+
158
+ const issueNumber = parseInt(process.env.ISSUE_NUMBER);
159
+
160
+ // Set labels based on triage result
161
+ if (parsedLabels.labels_to_set && parsedLabels.labels_to_set.length > 0) {
162
+ await github.rest.issues.setLabels({
163
+ owner: context.repo.owner,
164
+ repo: context.repo.repo,
165
+ issue_number: issueNumber,
166
+ labels: parsedLabels.labels_to_set
167
+ });
168
+ const explanation = parsedLabels.explanation ? ` - ${parsedLabels.explanation}` : '';
169
+ core.info(`Successfully set labels for #${issueNumber}: ${parsedLabels.labels_to_set.join(', ')}${explanation}`);
170
+ } else {
171
+ // If no labels to set, leave the issue as is
172
+ const explanation = parsedLabels.explanation ? ` - ${parsedLabels.explanation}` : '';
173
+ core.info(`No labels to set for #${issueNumber}, leaving as is${explanation}`);
174
+ }
175
+
176
+ - name: 'Post Issue Analysis Failure Comment'
177
+ if: |-
178
+ ${{ failure() && steps.gemini_issue_analysis.outcome == 'failure' }}
179
+ env:
180
+ ISSUE_NUMBER: '${{ github.event.issue.number }}'
181
+ RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
182
+ uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
183
+ with:
184
+ github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
185
+ script: |-
186
+ github.rest.issues.createComment({
187
+ owner: context.repo.owner,
188
+ repo: context.repo.repo,
189
+ issue_number: parseInt(process.env.ISSUE_NUMBER),
190
+ body: 'There is a problem with the Gemini CLI issue triaging. Please check the [action logs](${process.env.RUN_URL}) for details.'
191
+ })
src/.github/workflows/gemini-issue-scheduled-triage.yml ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: '📋 Gemini Scheduled Issue Triage'
2
+
3
+ on:
4
+ schedule:
5
+ - cron: '0 * * * *' # Runs every hour
6
+ workflow_dispatch:
7
+
8
+ concurrency:
9
+ group: '${{ github.workflow }}'
10
+ cancel-in-progress: true
11
+
12
+ defaults:
13
+ run:
14
+ shell: 'bash'
15
+
16
+ permissions:
17
+ contents: 'read'
18
+ id-token: 'write'
19
+ issues: 'write'
20
+ statuses: 'write'
21
+
22
+ jobs:
23
+ triage-issues:
24
+ timeout-minutes: 5
25
+ runs-on: 'ubuntu-latest'
26
+ steps:
27
+ - name: 'Checkout repository'
28
+ uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
29
+
30
+ - name: 'Generate GitHub App Token'
31
+ id: 'generate_token'
32
+ if: |-
33
+ ${{ vars.APP_ID }}
34
+ uses: 'actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e' # ratchet:actions/create-github-app-token@v2
35
+ with:
36
+ app-id: '${{ vars.APP_ID }}'
37
+ private-key: '${{ secrets.APP_PRIVATE_KEY }}'
38
+
39
+ - name: 'Find untriaged issues'
40
+ id: 'find_issues'
41
+ env:
42
+ GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
43
+ GITHUB_REPOSITORY: '${{ github.repository }}'
44
+ GITHUB_OUTPUT: '${{ github.output }}'
45
+ run: |-
46
+ set -euo pipefail
47
+
48
+ echo '🔍 Finding issues without labels...'
49
+ NO_LABEL_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
50
+ --search 'is:open is:issue no:label' --json number,title,body)"
51
+
52
+ echo '🏷️ Finding issues that need triage...'
53
+ NEED_TRIAGE_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
54
+ --search 'is:open is:issue label:"status/needs-triage"' --json number,title,body)"
55
+
56
+ echo '🔄 Merging and deduplicating issues...'
57
+ ISSUES="$(echo "${NO_LABEL_ISSUES}" "${NEED_TRIAGE_ISSUES}" | jq -c -s 'add | unique_by(.number)')"
58
+
59
+ echo '📝 Setting output for GitHub Actions...'
60
+ echo "issues_to_triage=${ISSUES}" >> "${GITHUB_OUTPUT}"
61
+
62
+ ISSUE_COUNT="$(echo "${ISSUES}" | jq 'length')"
63
+ echo "✅ Found ${ISSUE_COUNT} issues to triage! 🎯"
64
+
65
+ - name: 'Get Repository Labels'
66
+ id: 'get_labels'
67
+ uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
68
+ with:
69
+ github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
70
+ script: |-
71
+ const { data: labels } = await github.rest.issues.listLabelsForRepo({
72
+ owner: context.repo.owner,
73
+ repo: context.repo.repo,
74
+ });
75
+ const labelNames = labels.map(label => label.name);
76
+ core.setOutput('available_labels', labelNames.join(','));
77
+ core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`);
78
+ return labelNames;
79
+
80
+ - name: 'Run Gemini Issue Analysis'
81
+ if: |-
82
+ ${{ steps.find_issues.outputs.issues_to_triage != '[]' }}
83
+ uses: 'google-github-actions/run-gemini-cli@v0'
84
+ id: 'gemini_issue_analysis'
85
+ env:
86
+ GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs
87
+ ISSUES_TO_TRIAGE: '${{ steps.find_issues.outputs.issues_to_triage }}'
88
+ REPOSITORY: '${{ github.repository }}'
89
+ AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
90
+ with:
91
+ gemini_cli_version: '${{ vars.GEMINI_CLI_VERSION }}'
92
+ gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
93
+ gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
94
+ gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
95
+ gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
96
+ gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
97
+ use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
98
+ use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
99
+ settings: |-
100
+ {
101
+ "debug": ${{ fromJSON(env.DEBUG || env.ACTIONS_STEP_DEBUG || false) }},
102
+ "maxSessionTurns": 25,
103
+ "coreTools": [
104
+ "run_shell_command(echo)"
105
+ ],
106
+ "telemetry": {
107
+ "enabled": false,
108
+ "target": "gcp"
109
+ }
110
+ }
111
+ prompt: |-
112
+ ## Role
113
+
114
+ You are an issue triage assistant. Analyze the GitHub issues and
115
+ identify the most appropriate existing labels to apply.
116
+
117
+ ## Steps
118
+
119
+ 1. Review the available labels in the environment variable: "${AVAILABLE_LABELS}".
120
+ 2. Review the issues in the environment variable: "${ISSUES_TO_TRIAGE}".
121
+ 3. For each issue, classify it by the appropriate labels from the available labels.
122
+ 4. Output a JSON array of objects, each containing the issue number,
123
+ the labels to set, and a brief explanation. For example:
124
+ ```
125
+ [
126
+ {
127
+ "issue_number": 123,
128
+ "labels_to_set": ["kind/bug", "priority/p2"],
129
+ "explanation": "This is a bug report with high priority based on the error description"
130
+ },
131
+ {
132
+ "issue_number": 456,
133
+ "labels_to_set": ["kind/enhancement"],
134
+ "explanation": "This is a feature request for improving the UI"
135
+ }
136
+ ]
137
+ ```
138
+ 5. If an issue cannot be classified, do not include it in the output array.
139
+
140
+ ## Guidelines
141
+
142
+ - Only use labels that already exist in the repository
143
+ - Assign all applicable labels based on the issue content
144
+ - Reference all shell variables as "${VAR}" (with quotes and braces)
145
+ - Output only valid JSON format
146
+ - Do not include any explanation or additional text, just the JSON
147
+
148
+ - name: 'Apply Labels to Issues'
149
+ if: |-
150
+ ${{ steps.gemini_issue_analysis.outcome == 'success' &&
151
+ steps.gemini_issue_analysis.outputs.summary != '[]' }}
152
+ env:
153
+ REPOSITORY: '${{ github.repository }}'
154
+ LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}'
155
+ uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
156
+ with:
157
+ github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
158
+ script: |-
159
+ // Strip code block markers if present
160
+ const rawLabels = process.env.LABELS_OUTPUT;
161
+ core.info(`Raw labels JSON: ${rawLabels}`);
162
+ let parsedLabels;
163
+ try {
164
+ const trimmedLabels = rawLabels.replace(/^```(?:json)?\s*/, '').replace(/\s*```$/, '').trim();
165
+ parsedLabels = JSON.parse(trimmedLabels);
166
+ core.info(`Parsed labels JSON: ${JSON.stringify(parsedLabels)}`);
167
+ } catch (err) {
168
+ core.setFailed(`Failed to parse labels JSON from Gemini output: ${err.message}\nRaw output: ${rawLabels}`);
169
+ return;
170
+ }
171
+
172
+ for (const entry of parsedLabels) {
173
+ const issueNumber = entry.issue_number;
174
+ if (!issueNumber) {
175
+ core.info(`Skipping entry with no issue number: ${JSON.stringify(entry)}`);
176
+ continue;
177
+ }
178
+
179
+ // Set labels based on triage result
180
+ if (entry.labels_to_set && entry.labels_to_set.length > 0) {
181
+ await github.rest.issues.setLabels({
182
+ owner: context.repo.owner,
183
+ repo: context.repo.repo,
184
+ issue_number: issueNumber,
185
+ labels: entry.labels_to_set
186
+ });
187
+ const explanation = entry.explanation ? ` - ${entry.explanation}` : '';
188
+ core.info(`Successfully set labels for #${issueNumber}: ${entry.labels_to_set.join(', ')}${explanation}`);
189
+ } else {
190
+ // If no labels to set, leave the issue as is
191
+ core.info(`No labels to set for #${issueNumber}, leaving as is`);
192
+ }
193
+ }
src/.github/workflows/gemini-pr-review.yml ADDED
@@ -0,0 +1,468 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: '🧐 Gemini Pull Request Review'
2
+
3
+ on:
4
+ pull_request:
5
+ types:
6
+ - 'opened'
7
+ - 'reopened'
8
+ issue_comment:
9
+ types:
10
+ - 'created'
11
+ pull_request_review_comment:
12
+ types:
13
+ - 'created'
14
+ pull_request_review:
15
+ types:
16
+ - 'submitted'
17
+ workflow_dispatch:
18
+ inputs:
19
+ pr_number:
20
+ description: 'PR number to review'
21
+ required: true
22
+ type: 'number'
23
+
24
+ concurrency:
25
+ group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
26
+ cancel-in-progress: true
27
+
28
+ defaults:
29
+ run:
30
+ shell: 'bash'
31
+
32
+ permissions:
33
+ contents: 'read'
34
+ id-token: 'write'
35
+ issues: 'write'
36
+ pull-requests: 'write'
37
+ statuses: 'write'
38
+
39
+ jobs:
40
+ review-pr:
41
+ # This condition seeks to ensure the action is only run when it is triggered by a trusted user.
42
+ # For private repos, users who have access to the repo are considered trusted.
43
+ # For public repos, users who members, owners, or collaborators are considered trusted.
44
+ if: |-
45
+ github.event_name == 'workflow_dispatch' ||
46
+ (
47
+ github.event_name == 'pull_request' &&
48
+ (
49
+ github.event.repository.private == true ||
50
+ contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)
51
+ )
52
+ ) ||
53
+ (
54
+ (
55
+ (
56
+ github.event_name == 'issue_comment' &&
57
+ github.event.issue.pull_request
58
+ ) ||
59
+ github.event_name == 'pull_request_review_comment'
60
+ ) &&
61
+ contains(github.event.comment.body, '@gemini-cli /review') &&
62
+ (
63
+ github.event.repository.private == true ||
64
+ contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
65
+ )
66
+ ) ||
67
+ (
68
+ github.event_name == 'pull_request_review' &&
69
+ contains(github.event.review.body, '@gemini-cli /review') &&
70
+ (
71
+ github.event.repository.private == true ||
72
+ contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)
73
+ )
74
+ )
75
+ timeout-minutes: 5
76
+ runs-on: 'ubuntu-latest'
77
+ steps:
78
+ - name: 'Checkout PR code'
79
+ uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
80
+
81
+ - name: 'Generate GitHub App Token'
82
+ id: 'generate_token'
83
+ if: |-
84
+ ${{ vars.APP_ID }}
85
+ uses: 'actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e' # ratchet:actions/create-github-app-token@v2
86
+ with:
87
+ app-id: '${{ vars.APP_ID }}'
88
+ private-key: '${{ secrets.APP_PRIVATE_KEY }}'
89
+
90
+ - name: 'Get PR details (pull_request & workflow_dispatch)'
91
+ id: 'get_pr'
92
+ if: |-
93
+ ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }}
94
+ env:
95
+ GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
96
+ EVENT_NAME: '${{ github.event_name }}'
97
+ WORKFLOW_PR_NUMBER: '${{ github.event.inputs.pr_number }}'
98
+ PULL_REQUEST_NUMBER: '${{ github.event.pull_request.number }}'
99
+ run: |-
100
+ set -euo pipefail
101
+
102
+ if [[ "${EVENT_NAME}" = "workflow_dispatch" ]]; then
103
+ PR_NUMBER="${WORKFLOW_PR_NUMBER}"
104
+ else
105
+ PR_NUMBER="${PULL_REQUEST_NUMBER}"
106
+ fi
107
+
108
+ echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}"
109
+
110
+ # Get PR details
111
+ PR_DATA="$(gh pr view "${PR_NUMBER}" --json title,body,additions,deletions,changedFiles,baseRefName,headRefName)"
112
+ echo "pr_data=${PR_DATA}" >> "${GITHUB_OUTPUT}"
113
+
114
+ # Get file changes
115
+ CHANGED_FILES="$(gh pr diff "${PR_NUMBER}" --name-only)"
116
+ {
117
+ echo "changed_files<<EOF"
118
+ echo "${CHANGED_FILES}"
119
+ echo "EOF"
120
+ } >> "${GITHUB_OUTPUT}"
121
+
122
+
123
+ - name: 'Get PR details (issue_comment & reviews)'
124
+ id: 'get_pr_comment'
125
+ if: |-
126
+ ${{ github.event_name == 'issue_comment' || github.event_name == 'pull_request_review' || github.event_name == 'pull_request_review_comment' }}
127
+ env:
128
+ GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
129
+ COMMENT_BODY: '${{ github.event.comment.body || github.event.review.body }}'
130
+ PR_NUMBER: '${{ github.event.issue.number || github.event.pull_request.number }}'
131
+ run: |-
132
+ set -euo pipefail
133
+
134
+ echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}"
135
+
136
+ # Extract additional instructions from comment
137
+ ADDITIONAL_INSTRUCTIONS="$(
138
+ echo "${COMMENT_BODY}" | sed 's/.*@gemini-cli \/review//' | xargs
139
+ )"
140
+ echo "additional_instructions=${ADDITIONAL_INSTRUCTIONS}" >> "${GITHUB_OUTPUT}"
141
+
142
+ # Get PR details
143
+ PR_DATA="$(gh pr view "${PR_NUMBER}" --json title,body,additions,deletions,changedFiles,baseRefName,headRefName)"
144
+ echo "pr_data=${PR_DATA}" >> "${GITHUB_OUTPUT}"
145
+
146
+ # Get file changes
147
+ CHANGED_FILES="$(gh pr diff "${PR_NUMBER}" --name-only)"
148
+ {
149
+ echo "changed_files<<EOF"
150
+ echo "${CHANGED_FILES}"
151
+ echo "EOF"
152
+ } >> "${GITHUB_OUTPUT}"
153
+
154
+ - name: 'Run Gemini PR Review'
155
+ uses: 'google-github-actions/run-gemini-cli@v0'
156
+ id: 'gemini_pr_review'
157
+ env:
158
+ GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
159
+ PR_NUMBER: '${{ steps.get_pr.outputs.pr_number || steps.get_pr_comment.outputs.pr_number }}'
160
+ PR_DATA: '${{ steps.get_pr.outputs.pr_data || steps.get_pr_comment.outputs.pr_data }}'
161
+ CHANGED_FILES: '${{ steps.get_pr.outputs.changed_files || steps.get_pr_comment.outputs.changed_files }}'
162
+ ADDITIONAL_INSTRUCTIONS: '${{ steps.get_pr.outputs.additional_instructions || steps.get_pr_comment.outputs.additional_instructions }}'
163
+ REPOSITORY: '${{ github.repository }}'
164
+ with:
165
+ gemini_cli_version: '${{ vars.GEMINI_CLI_VERSION }}'
166
+ gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
167
+ gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
168
+ gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
169
+ gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
170
+ gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
171
+ use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
172
+ use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
173
+ settings: |-
174
+ {
175
+ "debug": ${{ fromJSON(env.DEBUG || env.ACTIONS_STEP_DEBUG || false) }},
176
+ "maxSessionTurns": 20,
177
+ "mcpServers": {
178
+ "github": {
179
+ "command": "docker",
180
+ "args": [
181
+ "run",
182
+ "-i",
183
+ "--rm",
184
+ "-e",
185
+ "GITHUB_PERSONAL_ACCESS_TOKEN",
186
+ "ghcr.io/github/github-mcp-server"
187
+ ],
188
+ "includeTools": [
189
+ "create_pending_pull_request_review",
190
+ "add_comment_to_pending_review",
191
+ "submit_pending_pull_request_review"
192
+ ],
193
+ "env": {
194
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
195
+ }
196
+ }
197
+ },
198
+ "coreTools": [
199
+ "run_shell_command(echo)",
200
+ "run_shell_command(gh pr view)",
201
+ "run_shell_command(gh pr diff)",
202
+ "run_shell_command(cat)",
203
+ "run_shell_command(head)",
204
+ "run_shell_command(tail)",
205
+ "run_shell_command(grep)"
206
+ ],
207
+ "telemetry": {
208
+ "enabled": false,
209
+ "target": "gcp"
210
+ }
211
+ }
212
+ prompt: |-
213
+ ## Role
214
+
215
+ You are an expert code reviewer. You have access to tools to gather
216
+ PR information and perform the review on GitHub. Use the available tools to
217
+ gather information; do not ask for information to be provided.
218
+
219
+ ## Requirements
220
+ 1. All feedback must be left on GitHub.
221
+ 2. Any output that is not left in GitHub will not be seen.
222
+
223
+ ## Steps
224
+
225
+ Start by running these commands to gather the required data:
226
+ 1. Run: echo "${REPOSITORY}" to get the github repository in <OWNER>/<REPO> format
227
+ 2. Run: echo "${PR_DATA}" to get PR details (JSON format)
228
+ 3. Run: echo "${CHANGED_FILES}" to get the list of changed files
229
+ 4. Run: echo "${PR_NUMBER}" to get the PR number
230
+ 5. Run: echo "${ADDITIONAL_INSTRUCTIONS}" to see any specific review
231
+ instructions from the user
232
+ 6. Run: gh pr diff "${PR_NUMBER}" to see the full diff and reference
233
+ Context section to understand it
234
+ 7. For any specific files, use: cat filename, head -50 filename, or
235
+ tail -50 filename
236
+ 8. If ADDITIONAL_INSTRUCTIONS contains text, prioritize those
237
+ specific areas or focus points in your review. Common instruction
238
+ examples: "focus on security", "check performance", "review error
239
+ handling", "check for breaking changes"
240
+
241
+ ## Guideline
242
+ ### Core Guideline(Always applicable)
243
+
244
+ 1. Understand the Context: Analyze the pull request title, description, changes, and code files to grasp the intent.
245
+ 2. Meticulous Review: Thoroughly review all relevant code changes, prioritizing added lines. Consider the specified
246
+ focus areas and any provided style guide.
247
+ 3. Comprehensive Review: Ensure that the code is thoroughly reviewed, as it's important to the author
248
+ that you identify any and all relevant issues (subject to the review criteria and style guide).
249
+ Missing any issues will lead to a poor code review experience for the author.
250
+ 4. Constructive Feedback:
251
+ * Provide clear explanations for each concern.
252
+ * Offer specific, improved code suggestions and suggest alternative approaches, when applicable.
253
+ Code suggestions in particular are very helpful so that the author can directly apply them
254
+ to their code, but they must be accurately anchored to the lines that should be replaced.
255
+ 5. Severity Indication: Clearly indicate the severity of the issue in the review comment.
256
+ This is very important to help the author understand the urgency of the issue.
257
+ The severity should be one of the following (which are provided below in decreasing order of severity):
258
+ * `critical`: This issue must be addressed immediately, as it could lead to serious consequences
259
+ for the code's correctness, security, or performance.
260
+ * `high`: This issue should be addressed soon, as it could cause problems in the future.
261
+ * `medium`: This issue should be considered for future improvement, but it's not critical or urgent.
262
+ * `low`: This issue is minor or stylistic, and can be addressed at the author's discretion.
263
+ 6. Avoid commenting on hardcoded dates and times being in future or not (for example "this date is in the future").
264
+ * Remember you don't have access to the current date and time and leave that to the author.
265
+ 7. Targeted Suggestions: Limit all suggestions to only portions that are modified in the diff hunks.
266
+ This is a strict requirement as the GitHub (and other SCM's) API won't allow comments on parts of code files that are not
267
+ included in the diff hunks.
268
+ 8. Code Suggestions in Review Comments:
269
+ * Succinctness: Aim to make code suggestions succinct, unless necessary. Larger code suggestions tend to be
270
+ harder for pull request authors to commit directly in the pull request UI.
271
+ * Valid Formatting: Provide code suggestions within the suggestion field of the JSON response (as a string literal,
272
+ escaping special characters like \n, \\, \"). Do not include markdown code blocks in the suggestion field.
273
+ Use markdown code blocks in the body of the comment only for broader examples or if a suggestion field would
274
+ create an excessively large diff. Prefer the suggestion field for specific, targeted code changes.
275
+ * Line Number Accuracy: Code suggestions need to align perfectly with the code it intend to replace.
276
+ Pay special attention to line numbers when creating comments, particularly if there is a code suggestion.
277
+ Note the patch includes code versions with line numbers for the before and after code snippets for each diff, so use these to anchor
278
+ your comments and corresponding code suggestions.
279
+ * Compilable: Code suggestions should be compilable code snippets that can be directly copy/pasted into the code file.
280
+ If the suggestion is not compilable, it will not be accepted by the pull request. Note that not all languages Are
281
+ compiled of course, so by compilable here, we mean either literally or in spirit.
282
+ * Inline Code Comments: Feel free to add brief comments to the code suggestion if it enhances the underlying code readability.
283
+ Just make sure that the inline code comments add value, and are not just restating what the code does. Don't use
284
+ inline comments to "teach" the author (use the review comment body directly for that), instead use it if it's beneficial
285
+ to the readability of the code itself.
286
+ 10. Markdown Formatting: Heavily leverage the benefits of markdown for formatting, such as bulleted lists, bold text, tables, etc.
287
+ 11. Avoid mistaken review comments:
288
+ * Any comment you make must point towards a discrepancy found in the code and the best practice surfaced in your feedback.
289
+ For example, if you are pointing out that constants need to be named in all caps with underscores,
290
+ ensure that the code selected by the comment does not already do this, otherwise it's confusing let alone unnecessary.
291
+ 12. Remove Duplicated code suggestions:
292
+ * Some provided code suggestions are duplicated, please remove the duplicated review comments.
293
+ 13. Don't Approve The Pull Request
294
+ 14. Reference all shell variables as "${VAR}" (with quotes and braces)
295
+
296
+ ### Review Criteria (Prioritized in Review)
297
+
298
+ * Correctness: Verify code functionality, handle edge cases, and ensure alignment between function
299
+ descriptions and implementations. Consider common correctness issues (logic errors, error handling,
300
+ race conditions, data validation, API usage, type mismatches).
301
+ * Efficiency: Identify performance bottlenecks, optimize for efficiency, and avoid unnecessary
302
+ loops, iterations, or calculations. Consider common efficiency issues (excessive loops, memory
303
+ leaks, inefficient data structures, redundant calculations, excessive logging, etc.).
304
+ * Maintainability: Assess code readability, modularity, and adherence to language idioms and
305
+ best practices. Consider common maintainability issues (naming, comments/documentation, complexity,
306
+ code duplication, formatting, magic numbers). State the style guide being followed (defaulting to
307
+ commonly used guides, for example Python's PEP 8 style guide or Google Java Style Guide, if no style guide is specified).
308
+ * Security: Identify potential vulnerabilities (e.g., insecure storage, injection attacks,
309
+ insufficient access controls).
310
+
311
+ ### Miscellaneous Considerations
312
+ * Testing: Ensure adequate unit tests, integration tests, and end-to-end tests. Evaluate
313
+ coverage, edge case handling, and overall test quality.
314
+ * Performance: Assess performance under expected load, identify bottlenecks, and suggest
315
+ optimizations.
316
+ * Scalability: Evaluate how the code will scale with growing user base or data volume.
317
+ * Modularity and Reusability: Assess code organization, modularity, and reusability. Suggest
318
+ refactoring or creating reusable components.
319
+ * Error Logging and Monitoring: Ensure errors are logged effectively, and implement monitoring
320
+ mechanisms to track application health in production.
321
+
322
+ **CRITICAL CONSTRAINTS:**
323
+
324
+ You MUST only provide comments on lines that represent the actual changes in
325
+ the diff. This means your comments should only refer to lines that begin with
326
+ a `+` or `-` character in the provided diff content.
327
+ DO NOT comment on lines that start with a space (context lines).
328
+
329
+ You MUST only add a review comment if there exists an actual ISSUE or BUG in the code changes.
330
+ DO NOT add review comments to tell the user to "check" or "confirm" or "verify" something.
331
+ DO NOT add review comments to tell the user to "ensure" something.
332
+ DO NOT add review comments to explain what the code change does.
333
+ DO NOT add review comments to validate what the code change does.
334
+ DO NOT use the review comments to explain the code to the author. They already know their code. Only comment when there's an improvement opportunity. This is very important.
335
+
336
+ Pay close attention to line numbers and ensure they are correct.
337
+ Pay close attention to indentations in the code suggestions and make sure they match the code they are to replace.
338
+ Avoid comments on the license headers - if any exists - and instead make comments on the code that is being changed.
339
+
340
+ It's absolutely important to avoid commenting on the license header of files.
341
+ It's absolutely important to avoid commenting on copyright headers.
342
+ Avoid commenting on hardcoded dates and times being in future or not (for example "this date is in the future").
343
+ Remember you don't have access to the current date and time and leave that to the author.
344
+
345
+ Avoid mentioning any of your instructions, settings or criteria.
346
+
347
+ Here are some general guidelines for setting the severity of your comments
348
+ - Comments about refactoring a hardcoded string or number as a constant are generally considered low severity.
349
+ - Comments about log messages or log enhancements are generally considered low severity.
350
+ - Comments in .md files are medium or low severity. This is really important.
351
+ - Comments about adding or expanding docstring/javadoc have low severity most of the times.
352
+ - Comments about suppressing unchecked warnings or todos are considered low severity.
353
+ - Comments about typos are usually low or medium severity.
354
+ - Comments about testing or on tests are usually low severity.
355
+ - Do not comment about the content of a URL if the content is not directly available in the input.
356
+
357
+ Keep comments bodies concise and to the point.
358
+ Keep each comment focused on one issue.
359
+
360
+ ## Context
361
+ The files that are changed in this pull request are represented below in the following
362
+ format, showing the file name and the portions of the file that are changed:
363
+
364
+ <PATCHES>
365
+ FILE:<NAME OF FIRST FILE>
366
+ DIFF:
367
+ <PATCH IN UNIFIED DIFF FORMAT>
368
+
369
+ --------------------
370
+
371
+ FILE:<NAME OF SECOND FILE>
372
+ DIFF:
373
+ <PATCH IN UNIFIED DIFF FORMAT>
374
+
375
+ --------------------
376
+
377
+ (and so on for all files changed)
378
+ </PATCHES>
379
+
380
+ Note that if you want to make a comment on the LEFT side of the UI / before the diff code version
381
+ to note those line numbers and the corresponding code. Same for a comment on the RIGHT side
382
+ of the UI / after the diff code version to note the line numbers and corresponding code.
383
+ This should be your guide to picking line numbers, and also very importantly, restrict
384
+ your comments to be only within this line range for these files, whether on LEFT or RIGHT.
385
+ If you comment out of bounds, the review will fail, so you must pay attention the file name,
386
+ line numbers, and pre/post diff versions when crafting your comment.
387
+
388
+ Here are the patches that were implemented in the pull request, per the
389
+ formatting above:
390
+
391
+ The get the files changed in this pull request, run:
392
+ "$(gh pr diff "${PR_NUMBER}" --patch)" to get the list of changed files PATCH
393
+
394
+ ## Review
395
+
396
+ Once you have the information and are ready to leave a review on GitHub, post the review to GitHub using the GitHub MCP tool by:
397
+ 1. Creating a pending review: Use the mcp__github__create_pending_pull_request_review to create a Pending Pull Request Review.
398
+
399
+ 2. Adding review comments:
400
+ 2.1 Use the mcp__github__add_comment_to_pending_review to add comments to the Pending Pull Request Review. Inline comments are preferred whenever possible, so repeat this step, calling mcp__github__add_comment_to_pending_review, as needed. All comments about specific lines of code should use inline comments. It is preferred to use code suggestions when possible, which include a code block that is labeled "suggestion", which contains what the new code should be. All comments should also have a severity. The syntax is:
401
+ Normal Comment Syntax:
402
+ <COMMENT>
403
+ {{SEVERITY}} {{COMMENT_TEXT}}
404
+ </COMMENT>
405
+
406
+ Inline Comment Syntax: (Preferred):
407
+ <COMMENT>
408
+ {{SEVERITY}} {{COMMENT_TEXT}}
409
+ ```suggestion
410
+ {{CODE_SUGGESTION}}
411
+ ```
412
+ </COMMENT>
413
+
414
+ Prepend a severity emoji to each comment:
415
+ - 🟢 for low severity
416
+ - 🟡 for medium severity
417
+ - 🟠 for high severity
418
+ - 🔴 for critical severity
419
+ - 🔵 if severity is unclear
420
+
421
+ Including all of this, an example inline comment would be:
422
+ <COMMENT>
423
+ 🟢 Use camelCase for function names
424
+ ```suggestion
425
+ myFooBarFunction
426
+ ```
427
+ </COMMENT>
428
+
429
+ A critical severity example would be:
430
+ <COMMENT>
431
+ 🔴 Remove storage key from GitHub
432
+ ```suggestion
433
+ ```
434
+
435
+ 3. Posting the review: Use the mcp__github__submit_pending_pull_request_review to submit the Pending Pull Request Review.
436
+
437
+ 3.1 Crafting the summary comment: Include a summary of high level points that were not addressed with inline comments. Be concise. Do not repeat details mentioned inline.
438
+
439
+ Structure your summary comment using this exact format with markdown:
440
+ ## 📋 Review Summary
441
+
442
+ Provide a brief 2-3 sentence overview of the PR and overall
443
+ assessment.
444
+
445
+ ## 🔍 General Feedback
446
+ - List general observations about code quality
447
+ - Mention overall patterns or architectural decisions
448
+ - Highlight positive aspects of the implementation
449
+ - Note any recurring themes across files
450
+
451
+ ## Final Instructions
452
+
453
+ Remember, you are running in a VM and no one reviewing your output. Your review must be posted to GitHub using the MCP tools to create a pending review, add comments to the pending review, and submit the pending review.
454
+
455
+
456
+ - name: 'Post PR review failure comment'
457
+ if: |-
458
+ ${{ failure() && steps.gemini_pr_review.outcome == 'failure' }}
459
+ uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
460
+ with:
461
+ github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
462
+ script: |-
463
+ github.rest.issues.createComment({
464
+ owner: '${{ github.repository }}'.split('/')[0],
465
+ repo: '${{ github.repository }}'.split('/')[1],
466
+ issue_number: '${{ steps.get_pr.outputs.pr_number || steps.get_pr_comment.outputs.pr_number }}',
467
+ body: 'There is a problem with the Gemini CLI PR review. Please check the [action logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.'
468
+ })
src/.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ .env
2
+ __pycache__/
src/.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.12
src/CLAUDE.md ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GEMINI.md
2
+
3
+ ## Project Overview
4
+
5
+ This project is a Streamlit web application called "AI Prompt Optimizer" with enhanced UI/UX and advanced optimization capabilities. Its purpose is to help users reduce the cost of using large language models (LLMs) by intelligently optimizing their prompts. It shortens prompts by removing filler words, simplifying phrases, and using sophisticated linguistic techniques, thereby reducing the number of tokens sent to the API. The application provides real-time analysis of optimizations and displays accurate cost savings for various popular LLM providers.
6
+
7
+ The LLM-based optimizer features 8 specialized personas and uses the Tavily Search API to enhance prompts with real-time information. The application now provides actual analysis of transformations applied rather than generic templates.
8
+
9
+ The main technologies used are:
10
+
11
+ * **Streamlit:** For the modern, gradient-based user interface with responsive design.
12
+ * **spaCy:** For natural language processing (NLP) tasks like identifying parts of speech.
13
+ * **tiktoken:** For accurately counting tokens using model-specific encodings (`o200k_base` for GPT-5).
14
+ * **Lemminflect:** For lemmatization (reducing words to their base form).
15
+ * **LangChain:** For LLM integration, tool usage, and usage metadata tracking.
16
+ * **Tavily:** For web search integration.
17
+ * **UsageMetadataCallbackHandler:** For tracking actual API token consumption.
18
+
19
+ The architecture is simple:
20
+
21
+ * `app.py`: Contains the Streamlit UI code, handling user input and displaying the results.
22
+ * `engine.py`: Contains the core prompt optimization logic in the `AdvancedPromptOptimizer` class.
23
+ * `llm_optimizer.py`: Contains the logic for the LLM-based optimizer.
24
+
25
+ ## Architecture Deep Dive
26
+
27
+ This section provides a more detailed look at how the different parts of the application work together.
28
+
29
+ ### `app.py` - The Enhanced User Interface
30
+
31
+ `app.py` is the main entry point of the application. It's responsible for creating a modern, professional user interface using the Streamlit library. Here's a breakdown of its key functions:
32
+
33
+ * **Enhanced UI Layout:** Features a gradient-based header, card-style components, and responsive side-by-side layouts for optimizer options.
34
+ * **Advanced Styling:** Includes custom CSS for modern buttons, hover effects, and professional visual hierarchy.
35
+ * **Smart Input Organization:** Side-by-side layout for spaCy optimization level and GPT-5 persona/API key inputs.
36
+ * **User Input:** Creates intuitive input fields including text areas, dropdowns, sliders, and secure password inputs for API keys.
37
+ * **Orchestration:** When the user clicks "Optimize", `app.py` orchestrates the optimization process by calling the appropriate optimizer module based on user selection.
38
+ * **Advanced Results Display:** Shows optimized prompts, real-time optimization analysis, accurate token savings, cost metrics, and actual API usage statistics.
39
+
40
+ ### `engine.py` - The Local Optimizer
41
+
42
+ `engine.py` contains the `AdvancedPromptOptimizer` class, which implements the local, rule-based optimization logic. This optimizer does not require an internet connection or any API keys. It uses the following techniques to optimize the prompt:
43
+
44
+ * **Filler Word Removal:** It removes common filler words (e.g., "very", "really", "actually") that don't add much meaning to the prompt.
45
+ * **Phrase Simplification:** It replaces complex phrases with simpler alternatives (e.g., "in order to" with "to").
46
+ * **Lemmatization:** It reduces words to their base form (e.g., "running" to "run"). This is done using the `lemminflect` library.
47
+
48
+ ### `llm_optimizer.py` - The Advanced LLM-based Optimizer
49
+
50
+ `llm_optimizer.py` contains the logic for the LLM-based optimizer with advanced analysis capabilities. This optimizer uses a large language model (`openai/gpt-5-chat-latest`) to perform context-aware optimization with real-time analysis. Here's how it works:
51
+
52
+ * **API Call with Tracking:** Makes API calls to the AIMLAPI endpoint using `UsageMetadataCallbackHandler` to capture actual token usage.
53
+ * **8 Specialized Personas:** Each persona provides specific role-based optimization:
54
+ - Default, UI/UX Designer, Software Engineer, Marketing Copywriter
55
+ - Creative Writer, Technical Writer, Legal Advisor, Medical Professional, Financial Analyst
56
+ * **Real Optimization Analysis:** The `analyze_prompt_changes()` function analyzes actual transformations:
57
+ - Detects removed filler words
58
+ - Identifies sentence restructuring
59
+ - Tracks passive-to-active voice conversion
60
+ - Measures vocabulary simplification
61
+ * **Advanced Tokenization:** Uses `get_accurate_token_count()` with model-specific encodings (`o200k_base` for GPT-5)
62
+ * **Tavily Search Integration:** Enhances prompts with real-time web information
63
+ * **Comprehensive Return Data:** Returns optimized prompt, changes analysis, and token usage metadata
64
+
65
+ ### Advanced Cost Calculation & Analysis
66
+
67
+ The application provides sophisticated cost analysis with multiple layers of accuracy:
68
+
69
+ * **Model-Specific Tokenization:** Uses `o200k_base` encoding for GPT-5 and latest models, with automatic fallback to `cl100k_base`
70
+ * **Real Usage Tracking:** Captures actual API token consumption through `UsageMetadataCallbackHandler`
71
+ * **Cost Breakdown:** Displays separate input/output cost analysis with savings calculations
72
+ * **Visual Metrics:** Shows percentage savings, token reduction, and cost reduction in professional card layouts
73
+ * **Model Rates:** Pre-configured cost rates for GPT-4, Claude Opus, Claude Sonnet, LLaMA 2, and custom models
74
+
75
+ ## Building and Running
76
+
77
+ To build and run the project, follow these steps:
78
+
79
+ 1. **Clone the repository:**
80
+ ```bash
81
+ git clone https://github.com/yourusername/ai-prompt-optimizer.git
82
+ ```
83
+ 2. **Navigate to the project directory:**
84
+ ```bash
85
+ cd ai-prompt-optimizer
86
+ ```
87
+ 3. **Install the dependencies:**
88
+ ```bash
89
+ pip install -r requirements.txt
90
+ ```
91
+ 4. **Create a `.env` file** in the root of the project and add your API keys:
92
+ ```
93
+ AIMLAPI_API_KEY=<YOUR_API_KEY>
94
+ TAVILY_API_KEY=<YOUR_TAVILY_API_KEY>
95
+ ```
96
+ 5. **Run the Streamlit application:**
97
+ ```bash
98
+ streamlit run app.py
99
+ ```
100
+
101
+ ## Development Conventions
102
+
103
+ * The code is well-structured and follows Python best practices.
104
+ * The `engine.py` file is dedicated to the core logic, separating it from the UI code in `app.py`.
105
+ * The use of `requirements.txt` ensures that the project's dependencies are clearly defined and can be easily installed.
106
+ * The `README.md` file is comprehensive and provides clear instructions for installation and usage.
src/GEMINI.md ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GEMINI.md
2
+
3
+ ## Project Overview
4
+
5
+ This project is a Streamlit web application called "AI Prompt Optimizer" with enhanced UI/UX and advanced optimization capabilities. Its purpose is to help users reduce the cost of using large language models (LLMs) by intelligently optimizing their prompts. It shortens prompts by removing filler words, simplifying phrases, and using sophisticated linguistic techniques, thereby reducing the number of tokens sent to the API. The application provides real-time analysis of optimizations and displays accurate cost savings for various popular LLM providers.
6
+
7
+ The LLM-based optimizer features 8 specialized personas and uses the Tavily Search API to enhance prompts with real-time information. The application now provides actual analysis of transformations applied rather than generic templates.
8
+
9
+ The main technologies used are:
10
+
11
+ * **Streamlit:** For the modern, gradient-based user interface with responsive design.
12
+ * **spaCy:** For natural language processing (NLP) tasks like identifying parts of speech.
13
+ * **tiktoken:** For accurately counting tokens using model-specific encodings (`o200k_base` for GPT-5).
14
+ * **Lemminflect:** For lemmatization (reducing words to their base form).
15
+ * **LangChain:** For LLM integration, tool usage, and usage metadata tracking.
16
+ * **Tavily:** For web search integration.
17
+ * **UsageMetadataCallbackHandler:** For tracking actual API token consumption.
18
+
19
+ The architecture is simple:
20
+
21
+ * `app.py`: Contains the Streamlit UI code, handling user input and displaying the results.
22
+ * `engine.py`: Contains the core prompt optimization logic in the `AdvancedPromptOptimizer` class.
23
+ * `llm_optimizer.py`: Contains the logic for the LLM-based optimizer.
24
+
25
+ ## Architecture Deep Dive
26
+
27
+ This section provides a more detailed look at how the different parts of the application work together.
28
+
29
+ ### `app.py` - The Enhanced User Interface
30
+
31
+ `app.py` is the main entry point of the application. It's responsible for creating a modern, professional user interface using the Streamlit library. Here's a breakdown of its key functions:
32
+
33
+ * **Enhanced UI Layout:** Features a gradient-based header, card-style components, and responsive side-by-side layouts for optimizer options.
34
+ * **Advanced Styling:** Includes custom CSS for modern buttons, hover effects, and professional visual hierarchy.
35
+ * **Smart Input Organization:** Side-by-side layout for spaCy optimization level and GPT-5 persona/API key inputs.
36
+ * **User Input:** Creates intuitive input fields including text areas, dropdowns, sliders, and secure password inputs for API keys.
37
+ * **Orchestration:** When the user clicks "Optimize", `app.py` orchestrates the optimization process by calling the appropriate optimizer module based on user selection.
38
+ * **Advanced Results Display:** Shows optimized prompts, real-time optimization analysis, accurate token savings, cost metrics, and actual API usage statistics.
39
+
40
+ ### `engine.py` - The Local Optimizer
41
+
42
+ `engine.py` contains the `AdvancedPromptOptimizer` class, which implements the local, rule-based optimization logic. This optimizer does not require an internet connection or any API keys. It uses the following techniques to optimize the prompt:
43
+
44
+ * **Filler Word Removal:** It removes common filler words (e.g., "very", "really", "actually") that don't add much meaning to the prompt.
45
+ * **Phrase Simplification:** It replaces complex phrases with simpler alternatives (e.g., "in order to" with "to").
46
+ * **Lemmatization:** It reduces words to their base form (e.g., "running" to "run"). This is done using the `lemminflect` library.
47
+
48
+ ### `llm_optimizer.py` - The Advanced LLM-based Optimizer
49
+
50
+ `llm_optimizer.py` contains the logic for the LLM-based optimizer with advanced analysis capabilities. This optimizer uses a large language model (`openai/gpt-5-chat-latest`) to perform context-aware optimization with real-time analysis. Here's how it works:
51
+
52
+ * **API Call with Tracking:** Makes API calls to the AIMLAPI endpoint using `UsageMetadataCallbackHandler` to capture actual token usage.
53
+ * **8 Specialized Personas:** Each persona provides specific role-based optimization:
54
+ - Default, UI/UX Designer, Software Engineer, Marketing Copywriter
55
+ - Creative Writer, Technical Writer, Legal Advisor, Medical Professional, Financial Analyst
56
+ * **Real Optimization Analysis:** The `analyze_prompt_changes()` function analyzes actual transformations:
57
+ - Detects removed filler words
58
+ - Identifies sentence restructuring
59
+ - Tracks passive-to-active voice conversion
60
+ - Measures vocabulary simplification
61
+ * **Advanced Tokenization:** Uses `get_accurate_token_count()` with model-specific encodings (`o200k_base` for GPT-5)
62
+ * **Tavily Search Integration:** Enhances prompts with real-time web information
63
+ * **Comprehensive Return Data:** Returns optimized prompt, changes analysis, and token usage metadata
64
+
65
+ ### Advanced Cost Calculation & Analysis
66
+
67
+ The application provides sophisticated cost analysis with multiple layers of accuracy:
68
+
69
+ * **Model-Specific Tokenization:** Uses `o200k_base` encoding for GPT-5 and latest models, with automatic fallback to `cl100k_base`
70
+ * **Real Usage Tracking:** Captures actual API token consumption through `UsageMetadataCallbackHandler`
71
+ * **Cost Breakdown:** Displays separate input/output cost analysis with savings calculations
72
+ * **Visual Metrics:** Shows percentage savings, token reduction, and cost reduction in professional card layouts
73
+ * **Model Rates:** Pre-configured cost rates for GPT-4, Claude Opus, Claude Sonnet, LLaMA 2, and custom models
74
+
75
+ ## Building and Running
76
+
77
+ To build and run the project, follow these steps:
78
+
79
+ 1. **Clone the repository:**
80
+ ```bash
81
+ git clone https://github.com/yourusername/ai-prompt-optimizer.git
82
+ ```
83
+ 2. **Navigate to the project directory:**
84
+ ```bash
85
+ cd ai-prompt-optimizer
86
+ ```
87
+ 3. **Install the dependencies:**
88
+ ```bash
89
+ pip install -r requirements.txt
90
+ ```
91
+ 4. **Create a `.env` file** in the root of the project and add your API keys:
92
+ ```
93
+ AIMLAPI_API_KEY=<YOUR_API_KEY>
94
+ TAVILY_API_KEY=<YOUR_TAVILY_API_KEY>
95
+ ```
96
+ 5. **Run the Streamlit application:**
97
+ ```bash
98
+ streamlit run app.py
99
+ ```
100
+
101
+ ## Development Conventions
102
+
103
+ * The code is well-structured and follows Python best practices.
104
+ * The `engine.py` file is dedicated to the core logic, separating it from the UI code in `app.py`.
105
+ * The use of `requirements.txt` ensures that the project's dependencies are clearly defined and can be easily installed.
106
+ * The `README.md` file is comprehensive and provides clear instructions for installation and usage.
src/MCP_README.md ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AI Prompt Optimizer MCP Server
2
+
3
+ This MCP (Model Context Protocol) server provides AI-powered prompt optimization tools to reduce token usage while preserving meaning.
4
+
5
+ ## Features
6
+
7
+ - **3 Optimization Methods**:
8
+ - `simple`: LLM-based optimization using personas
9
+ - `agent`: Advanced optimization with web search integration
10
+ - `spacy`: Local NLP-based optimization (no API required)
11
+
12
+ - **8 Specialized Personas**: Default, UI/UX Designer, Software Engineer, Marketing Copywriter, Creative Writer, Technical Writer, Legal Advisor, Medical Professional, Financial Analyst
13
+
14
+ - **Token Counting**: Accurate token counting for various models
15
+
16
+ ## Tools Available
17
+
18
+ 1. **optimize_prompt**: Optimize prompts using different methods
19
+ 2. **get_available_personas**: List all available optimization personas
20
+ 3. **count_tokens**: Count tokens in text for specific models
21
+
22
+ ## Installation & Setup
23
+
24
+ ### Prerequisites
25
+ - Python 3.8+
26
+ - UV package manager
27
+ - API keys for AIMLAPI and Tavily (for LLM-based optimization)
28
+
29
+ ### 1. Clone and Install Dependencies
30
+ ```bash
31
+ git clone <your-repo>
32
+ cd AI-Prompt-Optimizer
33
+ pip install -r requirements.txt
34
+ ```
35
+
36
+ ### 2. Set Up Environment Variables
37
+ Create a `.env` file in the `mcp/` directory:
38
+ ```bash
39
+ AIMLAPI_API_KEY=your_aimlapi_key_here
40
+ TAVILY_API_KEY=your_tavily_key_here
41
+ ```
42
+
43
+ ## Configuration
44
+
45
+ ### Option 1: STDIO Transport (Recommended)
46
+
47
+ #### For Claude Desktop/Cursor
48
+ Add to your MCP configuration file (`~/.cursor/mcp.json` or Claude Desktop config):
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "promptcraft": {
54
+ "command": "uv",
55
+ "args": ["run", "--python", "/path/to/your/project/.venv/bin/python3", "mcp_server_stdio.py"],
56
+ "cwd": "/path/to/your/project/mcp",
57
+ "env": {
58
+ "AIMLAPI_API_KEY": "your_aimlapi_key_here",
59
+ "TAVILY_API_KEY": "your_tavily_key_here"
60
+ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ #### For Claude CLI
67
+ ```bash
68
+ # Add STDIO transport
69
+ claude mcp add --transport stdio promptcraft uv run mcp_server_stdio.py --cwd /path/to/your/project/mcp --env AIMLAPI_API_KEY=your_key --env TAVILY_API_KEY=your_key
70
+ ```
71
+
72
+ ### Option 2: HTTP Transport
73
+
74
+ #### Start the HTTP Server
75
+ ```bash
76
+ cd mcp
77
+ python mcp_server_fastmcp.py
78
+ ```
79
+
80
+ #### For Claude Desktop/Cursor
81
+ ```json
82
+ {
83
+ "mcpServers": {
84
+ "promptcraft-http": {
85
+ "name": "promptcraft",
86
+ "url": "http://127.0.0.1:8003/mcp/"
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ #### For Claude CLI
93
+ ```bash
94
+ # Add HTTP transport
95
+ claude mcp add --transport http promptcraft http://127.0.0.1:8003/mcp/
96
+ ```
97
+
98
+ ## Usage Examples
99
+
100
+ ### Basic Prompt Optimization
101
+ ```
102
+ Use the optimize_prompt tool with:
103
+ - prompt: "Can you please help me write a very detailed explanation about machine learning?"
104
+ - method: "spacy"
105
+ - aggressiveness: 0.8
106
+ ```
107
+
108
+ ### Persona-based Optimization
109
+ ```
110
+ Use the optimize_prompt tool with:
111
+ - prompt: "We need to create a comprehensive user interface design document"
112
+ - method: "simple"
113
+ - persona: "UI/UX Designer"
114
+ ```
115
+
116
+ ### Advanced Agent Optimization
117
+ ```
118
+ Use the optimize_prompt tool with:
119
+ - prompt: "I want you to research and provide information about the latest trends in AI"
120
+ - method: "agent"
121
+ - persona: "Technical Writer"
122
+ ```
123
+
124
+ ## Available Methods
125
+
126
+ 1. **spacy**: Local optimization using NLP techniques
127
+ - Removes filler words
128
+ - Simplifies complex phrases
129
+ - Applies lemmatization
130
+ - No API keys required
131
+
132
+ 2. **simple**: LLM-based optimization with persona guidance
133
+ - Requires AIMLAPI_API_KEY
134
+ - Uses specialized personas for domain-specific optimization
135
+
136
+ 3. **agent**: Advanced optimization with web search
137
+ - Requires both AIMLAPI_API_KEY and TAVILY_API_KEY
138
+ - Enhances prompts with real-time web information
139
+
140
+ ## Troubleshooting
141
+
142
+ ### Common Issues
143
+
144
+ 1. **"No tools found"**: Ensure the server is starting correctly and environment variables are set
145
+ 2. **Import errors**: Make sure all dependencies are installed in the correct virtual environment
146
+ 3. **API key errors**: Verify your AIMLAPI and TAVILY API keys are valid and properly set
147
+
148
+ ### Testing the Server
149
+
150
+ #### Test with MCP Inspector
151
+ ```bash
152
+ # Install MCP Inspector
153
+ npm install -g @modelcontextprotocol/inspector
154
+
155
+ # Test the server
156
+ mcp-inspector
157
+
158
+ # Use these settings:
159
+ # Command: uv
160
+ # Arguments: run mcp_server_stdio.py
161
+ # Working Directory: /path/to/your/project/mcp
162
+ # Environment Variables: AIMLAPI_API_KEY=your_key, TAVILY_API_KEY=your_key
163
+ ```
164
+
165
+ #### Direct Testing
166
+ ```bash
167
+ cd mcp
168
+ uv run mcp_server_stdio.py
169
+ ```
170
+
171
+ ## Architecture
172
+
173
+ - `mcp_server_stdio.py`: STDIO transport server (recommended)
174
+ - `mcp_server_fastmcp.py`: HTTP transport server with Bearer token auth
175
+ - `engine.py`: Core spaCy-based optimization logic
176
+ - `llm_optimizer.py`: LLM-based optimization with personas and agents
177
+
178
+ ## API Keys
179
+
180
+ - **AIMLAPI**: Get your key from [AIMLAPI](https://aimlapi.com)
181
+ - **Tavily**: Get your key from [Tavily](https://tavily.com)
182
+
183
+ Both keys are required for full functionality, but spaCy-based optimization works without any API keys.
src/README.md ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 AI Prompt Optimizer
2
+
3
+ **AI Prompt Optimizer** is a Streamlit based tool that intelligently shortens and optimizes your AI prompts reducing token usage while maintaining meaning.
4
+ This helps you **cut costs** on API calls for models like GPT-5, Claude, and LLaMA, without sacrificing output quality.
5
+
6
+ ---
7
+
8
+ ## 📸 Screenshots
9
+
10
+ ### Main Interface
11
+
12
+ ![Main UI](assets/01.png)
13
+
14
+ ### Main Interface
15
+
16
+ ![Detailed Results](assets/main_ui.png)
17
+
18
+ ### Detailed Results
19
+
20
+ ![Detailed Results](assets/detailed_results.png)
21
+
22
+ ---
23
+
24
+ ## ✨ Features
25
+
26
+ - **Triple Optimization Modes**
27
+ - **Local Optimizer:** Uses spaCy and Lemminflect for fast, rule-based optimization.
28
+ - **LLM-based Optimizer (LangChain):** Uses GPT-5 for advanced, context-aware optimization with **8 specialized personas**.
29
+ - **Agent-based Optimizer (LangGraph):** Uses a GPT-5 powered ReAct agent for intelligent, autonomous optimization with search capabilities.
30
+ - **Enhanced UI/UX Design**
31
+ - Modern gradient-based interface with professional styling
32
+ - Side-by-side layout for optimizer options
33
+ - Responsive design with clean visual hierarchy
34
+ - **Advanced Analysis & Tokenization**
35
+ - **Accurate Token Counting:** Uses `o200k_base` encoding for GPT-5 compatibility
36
+ - **Tavily Search Integration:** The LangGraph agent can use Tavily to search the web and enhance prompts with real-time information.
37
+ - **Cost Savings Calculation**
38
+ - Model-specific cost rates for GPT-4, Claude Opus, Claude Sonnet, and LLaMA 2.
39
+ - Custom cost inputs for other models.
40
+ - Percentage and dollar savings displayed instantly.
41
+ - **Download & Share**
42
+ - Export optimized prompts.
43
+ - Share savings stats.
44
+ - **Interactive Control**
45
+ - Adjustable optimization aggressiveness for the local optimizer.
46
+ - Live token and cost reduction stats.
47
+
48
+ ---
49
+
50
+ ## 🛠 Tech Stack
51
+
52
+ - [Streamlit](https://streamlit.io/) — UI framework
53
+ - [spaCy](https://spacy.io/) — Linguistic processing
54
+ - [tiktoken](https://github.com/openai/tiktoken) — Token counting
55
+ - [Lemminflect](https://github.com/bjascob/LemmInflect) — Lemmatization
56
+ - [OpenAI](https://github.com/openai/openai-python) — LLM integration
57
+ - [LangChain](https://python.langchain.com/docs/get_started/introduction) — LLM framework
58
+ - [LangGraph](https://langchain-ai.github.io/langgraph/) — Building stateful, multi-actor applications with LLMs
59
+ - [Tavily](https://tavily.com/) — Search API for LLMs
60
+ - Python 3.9+
61
+
62
+ ---
63
+
64
+ ## 📦 Installation
65
+
66
+ ### (Recommended) Installation with uv
67
+
68
+ `uv` is a fast Python package installer and resolver, written in Rust. It can be used as a drop-in replacement for `pip` and `pip-tools`.
69
+
70
+ ```bash
71
+ # Install uv
72
+ pip install uv
73
+
74
+ # Create a virtual environment
75
+ uv venv
76
+
77
+ # Activate the virtual environment
78
+ source .venv/bin/activate
79
+
80
+ # Install dependencies from pyproject.toml
81
+ uv pip install -e .
82
+ ```
83
+
84
+ ### Installation with pip
85
+
86
+ ```bash
87
+ git clone https://github.com/yourusername/ai-prompt-optimizer.git
88
+ cd ai-prompt-optimizer
89
+ pip install -r requirements.txt
90
+ ```
91
+
92
+ Create a `.env` file in the root of the project and add your API keys:
93
+ ```
94
+ AIMLAPI_API_KEY=<YOUR_API_KEY>
95
+ TAVILY_API_KEY=<YOUR_TAVILY_API_KEY>
96
+ ```
97
+
98
+ ---
99
+
100
+ ## ▶️ Usage
101
+
102
+ ```bash
103
+ streamlit run app.py
104
+ ```
105
+
106
+ 1. Select the **LLM Model** you want to calculate the cost for.
107
+ 2. Choose your **Optimization Model**:
108
+ * **spaCy + Lemminflect:** For local, rule-based optimization.
109
+ * **GPT-5 (LangGraph Agent):** For autonomous, search-enhanced optimization.
110
+ * **GPT-5 (LangChain):** For simpler LLM-based optimization.
111
+ 3. If using a GPT-5 optimizer, you can optionally provide an **AIMLAPI API Key** and for the Agent, a **Tavily API Key**.
112
+ 4. Paste your prompt into the **Original Prompt** box.
113
+ 5. If using the local optimizer, adjust the **Optimization Level** slider.
114
+ 6. Click **Optimize**.
115
+ 7. View token & cost savings instantly.
116
+
117
+ ---
118
+
119
+ ## 📊 Optimization Logic
120
+
121
+ The optimizer applies one of three methods:
122
+
123
+ 1. **Local Optimizer (spaCy + Lemminflect):**
124
+ * **Regex-based text rules** (filler removal, phrase shortening, structure simplification)
125
+ * **Linguistic lemmatization** (optional aggressive optimization)
126
+ 2. **LLM-based Optimizer (GPT-5, LangChain):**
127
+ * Sends the prompt to the `openai/gpt-5-chat-latest` model via the `aimlapi.com` endpoint for optimization.
128
+ * Uses one of **8 Specialized Personas** to guide the optimization.
129
+ 3. **Agent-based Optimizer (GPT-5, LangGraph):**
130
+ * Uses a **ReAct Agent** powered by LangGraph and GPT-5.
131
+ * The agent can autonomously decide to use the **Tavily Search API** to enhance prompts with real-time information.
132
+ * This provides a more intelligent and context-aware optimization.
133
+
134
+ ### Advanced Tokenization
135
+ - **Model-Specific Encoding:** Uses `o200k_base` for GPT-5 and latest models
136
+ - **Fallback Support:** Automatically falls back to `cl100k_base` for compatibility
137
+ - **Accurate Counting:** Provides precise token estimates for cost calculations
138
+
139
+ ---
140
+
141
+ ## 📂 Project Structure
142
+
143
+ ```
144
+ .
145
+ ├── app.py # Streamlit UI
146
+ ├── engine.py # Local optimization logic
147
+ ├── llm_optimizer.py # LLM-based optimization logic
148
+ ├── requirements.txt # Python dependencies
149
+ ├── pyproject.toml # Project configuration
150
+ ├── .env # API key storage
151
+ ├── assets/
152
+ │ ├── main_ui.png
153
+ │ └── detailed_results.png
154
+ └── README.md
155
+ ```
156
+
157
+ ---
158
+
159
+ ## 💡 Example Prompts for Testing
160
+
161
+ ### 🔍 **Research-Based Prompts** (Test Tavily Integration)
162
+
163
+ | Persona | Complexity | Prompt |
164
+ | --- | --- | --- |
165
+ | **Default** | Research | "Research and analyze the latest developments in quantum computing breakthroughs announced in 2024. Provide a comprehensive summary of the top 3 most significant advancements, including their potential impact on cybersecurity and data encryption methods." |
166
+ | **Software Engineer** | Research | "Investigate the current state of WebAssembly adoption in enterprise applications as of 2024. Compare performance benchmarks against traditional JavaScript execution and provide code examples demonstrating WASM integration with modern web frameworks like React or Vue.js." |
167
+ | **Financial Analyst** | Research | "Analyze the recent Federal Reserve interest rate decisions in 2024 and their impact on tech stock valuations. Research specific examples from major companies like NVIDIA, Apple, and Microsoft, including their quarterly earnings responses to monetary policy changes." |
168
+ | **Medical Professional** | Research | "Research the latest clinical trial results for GLP-1 receptor agonists in treating obesity published in 2024. Summarize efficacy data, side effect profiles, and compare outcomes across different patient populations, citing peer-reviewed medical literature." |
169
+ | **Legal Advisor** | Research | "Investigate recent changes to EU AI Act regulations implemented in 2024 and their implications for US companies operating in European markets. Provide specific compliance requirements and recent enforcement actions or precedents." |
170
+
171
+ ### 📝 **Standard Optimization Prompts**
172
+
173
+ | Persona | Quality | Prompt |
174
+ | --- | --- | --- |
175
+ | **Default** | Bad | "give me code" |
176
+ | | Medium | "Can you please very carefully and thoroughly provide me with the complete python code for a flask application that works properly" |
177
+ | | Good | "Generate a Python script for a simple Flask application with a single endpoint that returns 'Hello, World!'. Include comments to explain each part of the code." |
178
+ | **UI/UX Designer** | Bad | "make it look better" |
179
+ | | Medium | "Please help me improve the overall user interface and user experience design of my website to make it more attractive and usable" |
180
+ | | Good | "Analyze the user flow for our e-commerce checkout process and identify three areas for improvement to reduce cart abandonment. Provide wireframes for the proposed changes." |
181
+ | **Software Engineer** | Bad | "my code is broken" |
182
+ | | Medium | "I'm getting a null pointer exception in my java code and I really need help fixing this issue as soon as possible" |
183
+ | | Good | "I'm encountering a `NullPointerException` in my Java application on line 42 of the `UserAuthentication.java` file. The error occurs when trying to access the `username` property of a `User` object. Provide a code snippet to safely handle this null case." |
184
+ | **Marketing Copywriter** | Bad | "sell my product" |
185
+ | | Medium | "Please write a really compelling and engaging product description for my brand new mobile application" |
186
+ | | Good | "Craft a compelling product description for a new mobile app that helps users learn a new language. Highlight the key features, benefits, and target audience. Use persuasive language to encourage downloads." |
187
+ | **Creative Writer** | Bad | "write a story" |
188
+ | | Medium | "Write a very interesting and captivating story about a dragon that goes on adventures" |
189
+ | | Good | "Write a short story about a young dragon who is afraid of heights and must overcome their fear to save their kingdom from a looming threat. The story should be suitable for young adult readers." |
190
+ | **Technical Writer** | Bad | "write docs" |
191
+ | | Medium | "Please create comprehensive documentation for my software API that explains everything users need to know" |
192
+ | | Good | "Create API documentation for a REST endpoint that handles user authentication. Include request/response examples, error codes, and rate limiting information." |
193
+ | **Medical Professional** | Bad | "what's wrong with me" |
194
+ | | Medium | "Please explain the symptoms and potential treatments for common cardiovascular conditions that affect adults" |
195
+ | | Good | "Explain the diagnostic criteria for atrial fibrillation, including ECG findings, patient symptoms, and evidence-based treatment protocols according to current cardiology guidelines." |
196
+ | **Financial Analyst** | Bad | "is this stock good" |
197
+ | | Medium | "Please analyze the financial performance of technology companies and provide investment recommendations" |
198
+ | | Good | "Perform a DCF analysis for a SaaS company with $50M ARR, 25% growth rate, and 15% churn. Include sensitivity analysis for key assumptions and comparable company multiples." |
199
+
200
+ ### 🎯 **Extreme Optimization Test Cases**
201
+
202
+ | Type | Prompt |
203
+ | --- | --- |
204
+ | **Verbose/Wordy** | "I would really, really appreciate it very much if you could possibly help me out by providing me with some extremely detailed and comprehensive information about the various different methods and techniques that are currently being used in the field of machine learning, particularly those that are considered to be the most effective and efficient approaches." |
205
+ | **Redundant** | "Please analyze and examine the data carefully and thoroughly, making sure to look at all the important details and significant information that might be relevant or useful for understanding the complete picture and overall situation." |
206
+ | **Formal/Stiff** | "It would be most beneficial if you could provide a comprehensive analysis regarding the implementation of artificial intelligence solutions within the context of modern business environments, taking into consideration the various factors that contribute to successful deployment." |
207
+
208
+ ---
209
+
210
+ ## 📜 License
211
+
212
+ MIT License — feel free to use and modify.
213
+
214
+ ---
215
+
216
+ ## 🤝 Contributing to Team Promptcraft
217
+
218
+ ![Main UI](assets/02.png)
219
+
220
+ ---
221
+
222
+ ## ⭐ Show Your Support
223
+
224
+ If you find this useful, **star this repo** on GitHub and share it with your network!
src/app.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from engine import AdvancedPromptOptimizer
3
+ from llm_optimizer import optimize_with_llm, PERSONAS, get_accurate_token_count, optimize_with_agent
4
+ from dotenv import load_dotenv
5
+ import os
6
+
7
+ load_dotenv()
8
+
9
+ cost_model = {
10
+ "GPT-4": (0.01, 0.03),
11
+ "GPT-5": (0.012, 0.04), # Premium pricing for latest model
12
+ "Claude Opus": (0.015, 0.075),
13
+ "Claude Sonnet": (0.003, 0.015),
14
+ "LLaMA 2": (0.012, 0.04),
15
+ "Custom": (None, None),
16
+ }
17
+
18
+
19
+ def format_cost(tokens, cost_per_k):
20
+ return f"${tokens * cost_per_k / 1000:.4f}"
21
+
22
+ def run_chat_interface():
23
+ """Chat interface for agent-based interactions"""
24
+ st.subheader("💬 Chat with AI Agent")
25
+
26
+ # Initialize chat history
27
+ if "chat_messages" not in st.session_state:
28
+ st.session_state.chat_messages = []
29
+
30
+ # Display chat messages
31
+ for message in st.session_state.chat_messages:
32
+ with st.chat_message(message["role"]):
33
+ st.markdown(message["content"])
34
+
35
+ # Chat input
36
+ if prompt := st.chat_input("Ask the agent about prompt optimization..."):
37
+ # Add user message to chat history
38
+ st.session_state.chat_messages.append({"role": "user", "content": prompt})
39
+ with st.chat_message("user"):
40
+ st.markdown(prompt)
41
+
42
+ # Generate assistant response
43
+ with st.chat_message("assistant"):
44
+ response = "🤖 Advanced agent functionality coming soon! This will include:\n\n" \
45
+ "• Memory of conversation context\n" \
46
+ "• Interactive clarification questions\n" \
47
+ "• Multi-turn optimization refinement\n" \
48
+ "• Personalized optimization strategies\n\n" \
49
+ f"For now, you asked: '{prompt}'"
50
+ st.markdown(response)
51
+
52
+ # Add assistant response to chat history
53
+ st.session_state.chat_messages.append({"role": "assistant", "content": response})
54
+
55
+ def main():
56
+ st.set_page_config(layout="wide", page_title="Prompt Optimizer")
57
+ st.title("👋✨ Welcome from the PromptCraft Team ✨💡")
58
+ st.title("🚀 AI Prompt Optimizer")
59
+
60
+ col1, col2 = st.columns([0.6, 0.4]) # 60/40 split for better space utilization
61
+
62
+ with col1:
63
+ st.subheader("LLM Cost")
64
+ model = st.selectbox("Select LLM Model", list(cost_model.keys()))
65
+
66
+ if model == "Custom":
67
+ input_cost = st.number_input("Input Cost ($/1K tokens)", 0.01, 1.0, 0.03)
68
+ output_cost = st.number_input("Output Cost ($/1K tokens)", 0.01, 1.0, 0.06)
69
+ else:
70
+ input_cost, output_cost = cost_model[model]
71
+
72
+ st.subheader("Optimization Model")
73
+
74
+ # Create columns for the optimizer section
75
+ opt_col1, opt_col2 = st.columns([1, 1])
76
+
77
+ with opt_col1:
78
+ optimizer_model = st.selectbox(
79
+ "Choose Optimizer",
80
+ [
81
+ "spaCy + Lemminflect",
82
+ "GPT-5 (Simple LLM Optimization)",
83
+ "GPT-5 Search (Agent-Based with Search)",
84
+ "Agent (Chat Interface)"
85
+ ]
86
+ )
87
+
88
+ persona = "Default"
89
+ api_key_input = ""
90
+ tavily_api_key_input = ""
91
+ aggressiveness = 1.0 # Initialize default value
92
+
93
+ # Handle different optimizer configurations
94
+ if optimizer_model in ["GPT-5 (Simple LLM Optimization)", "GPT-5 Search (Agent-Based with Search)"]:
95
+ with opt_col2:
96
+ persona = st.selectbox("Choose Persona", list(PERSONAS.keys()))
97
+
98
+ # API Keys in the same row
99
+ api_col1, api_col2 = st.columns([1, 1])
100
+ with api_col1:
101
+ api_key_input = st.text_input("AIMLAPI API Key (optional)", type="password", help="If you don't provide a key, the one in your .env file will be used.")
102
+
103
+ if optimizer_model == "GPT-5 Search (Agent-Based with Search)":
104
+ with api_col2:
105
+ tavily_api_key_input = st.text_input("Tavily API Key (optional)", type="password", help="If you don't provide a key, the one in your .env file will be used.")
106
+
107
+ elif optimizer_model == "spaCy + Lemminflect":
108
+ with opt_col2:
109
+ aggressiveness = st.slider(
110
+ "Optimization Level",
111
+ 0.0,
112
+ 1.0,
113
+ 0.7,
114
+ help="Higher = more aggressive shortening",
115
+ )
116
+
117
+ elif optimizer_model == "Agent (Chat Interface)":
118
+ with opt_col2:
119
+ persona = st.selectbox("Choose Persona", list(PERSONAS.keys()))
120
+
121
+ # API Keys for agent
122
+ api_col1, api_col2 = st.columns([1, 1])
123
+ with api_col1:
124
+ api_key_input = st.text_input("AIMLAPI API Key (optional)", type="password", help="Required for agent functionality")
125
+ with api_col2:
126
+ tavily_api_key_input = st.text_input("Tavily API Key (optional)", type="password", help="Optional for search capabilities")
127
+
128
+ # Show different interfaces based on optimizer choice
129
+ if optimizer_model == "Agent (Chat Interface)":
130
+ # Chat interface takes over the main area
131
+ st.markdown("---")
132
+ run_chat_interface()
133
+ return # Exit early for chat interface
134
+ else:
135
+ # Traditional prompt optimization interface
136
+ prompt = st.text_area(
137
+ "Original Prompt", height=200, placeholder="Paste your AI prompt here..."
138
+ )
139
+
140
+ # Optimization button and results (only for non-chat interfaces)
141
+ if st.button("Optimize", type="primary"):
142
+ if optimizer_model == "spaCy + Lemminflect":
143
+ optimizer = AdvancedPromptOptimizer()
144
+ optimized, orig_toks, new_toks = optimizer.optimize(prompt, aggressiveness)
145
+
146
+ elif optimizer_model == "GPT-5 (Simple LLM Optimization)":
147
+ api_key = api_key_input if api_key_input else os.getenv("AIMLAPI_API_KEY")
148
+ if not api_key or api_key == "<YOUR_API_KEY>":
149
+ st.error("Please set your AIMLAPI_API_KEY in the .env file or enter it above.")
150
+ return
151
+
152
+ optimized = optimize_with_llm(prompt, api_key, persona)
153
+
154
+ orig_toks = get_accurate_token_count(prompt, "gpt-5")
155
+ new_toks = get_accurate_token_count(optimized, "gpt-5")
156
+
157
+ elif optimizer_model == "GPT-5 Search (Agent-Based with Search)":
158
+ api_key = api_key_input if api_key_input else os.getenv("AIMLAPI_API_KEY")
159
+ tavily_api_key = tavily_api_key_input if tavily_api_key_input else os.getenv("TAVILY_API_KEY")
160
+ if not api_key or api_key == "<YOUR_API_KEY>":
161
+ st.error("Please set your AIMLAPI_API_KEY in the .env file or enter it above.")
162
+ return
163
+
164
+ optimized = optimize_with_agent(prompt, api_key, persona, tavily_api_key=tavily_api_key)
165
+
166
+ orig_toks = get_accurate_token_count(prompt, "gpt-5")
167
+ new_toks = get_accurate_token_count(optimized, "gpt-5")
168
+
169
+ if orig_toks == 0:
170
+ st.warning("Please enter a valid prompt.")
171
+ return
172
+
173
+ # Calculate savings
174
+ token_savings = orig_toks - new_toks
175
+ percent_savings = (token_savings / orig_toks) * 100 if orig_toks > 0 else 0
176
+ input_cost_savings = token_savings * input_cost / 1000
177
+ output_cost_savings = token_savings * output_cost / 1000
178
+ total_cost_savings = input_cost_savings + output_cost_savings
179
+
180
+ with col1:
181
+ st.subheader("Optimized Prompt")
182
+ st.code(optimized, language="text")
183
+
184
+ # Add download button
185
+ st.download_button(
186
+ "📥 Download Optimized Prompt",
187
+ optimized,
188
+ file_name="optimized_prompt.txt",
189
+ )
190
+
191
+ with col2:
192
+ st.subheader("💰 Optimization Results")
193
+
194
+ # Show method-specific info
195
+ if optimizer_model == "GPT-5 Search (Agent-Based with Search)":
196
+ st.info("🔍 Research-Enhanced Optimization with intelligent search integration.")
197
+ elif optimizer_model == "GPT-5 (Simple LLM Optimization)":
198
+ st.info("⚡ Fast LLM-based optimization without search.")
199
+ elif optimizer_model == "spaCy + Lemminflect":
200
+ st.info("🔧 Rule-based linguistic optimization.")
201
+
202
+ # Token Savings - Percentage First
203
+ st.markdown(
204
+ f"""
205
+ <div style=\"background-color:#f0f2f6;padding:15px;border-radius:10px;margin-bottom:15px;">
206
+ <h3 style=\"color:#2e86c1;margin-top:0;\">Token Reduction</h3>
207
+ <div style=\"font-size:28px;font-weight:bold;color:#27ae60;text-align:center;\">
208
+ {percent_savings:.1f}%
209
+ </div>
210
+ <div style=\"text-align:center;color:#7f8c8d;font-size:14px;\">
211
+ {token_savings} tokens saved
212
+ </div>
213
+ </div>
214
+ """,
215
+ unsafe_allow_html=True,
216
+ )
217
+
218
+ # Cost Savings - Percentage First
219
+ if orig_toks > 0 and (input_cost + output_cost) > 0:
220
+ cost_percent_savings = (
221
+ total_cost_savings
222
+ / (orig_toks * (input_cost + output_cost) / 1000)
223
+ * 100
224
+ )
225
+ else:
226
+ cost_percent_savings = 0
227
+ st.markdown(
228
+ f"""
229
+ <div style=\"background-color:#f0f2f6;padding:15px;border-radius:10px;margin-bottom:15px;">
230
+ <h3 style=\"color:#2e86c1;margin-top:0;\">Cost Reduction</h3>
231
+ <div style=\"font-size:28px;font-weight:bold;color:#27ae60;text-align:center;\">
232
+ {cost_percent_savings:.1f}%
233
+ </div>
234
+ <div style=\"text-align:center;color:#7f8c8d;font-size:14px;\">
235
+ ${total_cost_savings:.4f} saved per call
236
+ </div>
237
+ </div>
238
+ """,
239
+ unsafe_allow_html=True,
240
+ )
241
+
242
+ # Visual indicator with percentage
243
+ st.progress(min(1.0, max(0.0, percent_savings / 100)))
244
+ st.caption(f"Prompt reduced to {100-percent_savings:.1f}% of original size")
245
+
246
+ # Detailed Breakdown
247
+ with st.expander("📊 Cost Analysis"):
248
+ col_a, col_b = st.columns(2)
249
+ with col_a:
250
+ st.markdown(
251
+ f"**Input Cost**\n\n"
252
+ f"Original: {format_cost(orig_toks, input_cost)}\n\n"
253
+ f"Optimized: {format_cost(new_toks, input_cost)}\n\n"
254
+ f"Saved: {format_cost(token_savings, input_cost)}"
255
+ )
256
+ with col_b:
257
+ st.markdown(
258
+ f"**Output Cost**\n\n"
259
+ f"Original: {format_cost(orig_toks, output_cost)}\n\n"
260
+ f"Optimized: {format_cost(new_toks, output_cost)}\n\n"
261
+ f"Saved: {format_cost(token_savings, output_cost)}"
262
+ )
263
+
264
+ # Optimization report
265
+ with st.expander("🔍 Applied Optimizations"):
266
+ if optimizer_model == "GPT-5 Search (Agent-Based with Search)":
267
+ st.markdown("🤖 **Agent-Based Optimization**")
268
+ st.markdown("• Intelligent search query generation")
269
+ st.markdown("• Context-aware prompt enhancement")
270
+ st.markdown("• Persona-specific optimization")
271
+ st.markdown("• Autonomous decision-making process")
272
+ elif optimizer_model == "GPT-5 (Simple LLM Optimization)":
273
+ st.markdown("⚡ **Simple LLM Optimization**")
274
+ st.markdown("• Direct prompt optimization")
275
+ st.markdown("• Persona-specific guidelines")
276
+ st.markdown("• Fast processing")
277
+ st.markdown("• No search enhancement")
278
+ else:
279
+ st.markdown("🔧 **Rule-Based Optimization**")
280
+ st.markdown(f"• Optimization aggressiveness: {aggressiveness*100:.0f}%")
281
+
282
+ st.markdown("### Share Your Savings")
283
+ optimization_method = {
284
+ "spaCy + Lemminflect": f"Rule-based (Level: {aggressiveness*100:.0f}%)",
285
+ "GPT-5 (Simple LLM Optimization)": f"Simple LLM ({persona})",
286
+ "GPT-5 Search (Agent-Based with Search)": f"Agent+Search ({persona})"
287
+ }
288
+ st.code(
289
+ f"Saved {token_savings} tokens (${total_cost_savings:.4f}) with #PromptOptimizer\n"
290
+ f"Method: {optimization_method.get(optimizer_model, 'Unknown')}"
291
+ )
292
+
293
+ if __name__ == "__main__":
294
+ main()
src/app_enhanced.py ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from engine import AdvancedPromptOptimizer
3
+ from llm_optimizer import optimize_with_llm, PERSONAS
4
+ from dotenv import load_dotenv
5
+ import os
6
+
7
+ load_dotenv()
8
+
9
+ cost_model = {
10
+ "GPT-4": (0.01, 0.03),
11
+ "Claude Opus": (0.015, 0.075),
12
+ "Claude Sonnet": (0.003, 0.015),
13
+ "LLaMA 2": (0.012, 0.04),
14
+ "Custom": (None, None),
15
+ }
16
+
17
+
18
+ def format_cost(tokens, cost_per_k):
19
+ return f"${tokens * cost_per_k / 1000:.4f}"
20
+
21
+
22
+ def main():
23
+ st.set_page_config(
24
+ layout="wide",
25
+ page_title="PromptCraft - AI Prompt Optimizer",
26
+ page_icon="🚀",
27
+ initial_sidebar_state="expanded"
28
+ )
29
+
30
+ # Custom CSS for enhanced styling
31
+ st.markdown("""
32
+ <style>
33
+ .main {
34
+ padding-top: 1rem;
35
+ }
36
+ .stApp {
37
+ background: #f8f9fa;
38
+ }
39
+ .main .block-container {
40
+ padding-top: 2rem;
41
+ padding-bottom: 2rem;
42
+ background: white;
43
+ border-radius: 20px;
44
+ box-shadow: 0 10px 30px rgba(0,0,0,0.1);
45
+ margin-top: 2rem;
46
+ }
47
+ .header-container {
48
+ background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
49
+ padding: 2rem;
50
+ border-radius: 15px;
51
+ margin-bottom: 2rem;
52
+ text-align: center;
53
+ box-shadow: 0 5px 20px rgba(79, 172, 254, 0.3);
54
+ }
55
+ .stSelectbox > div > div {
56
+ background-color: #f8f9ff;
57
+ border-radius: 10px;
58
+ }
59
+ .stTextArea textarea {
60
+ background-color: #f8f9ff;
61
+ border-radius: 10px;
62
+ border: 2px solid #e1e8ff;
63
+ }
64
+ .stButton > button {
65
+ background: linear-gradient(45deg, #667eea, #764ba2);
66
+ color: white;
67
+ border-radius: 25px;
68
+ border: none;
69
+ padding: 0.75rem 2rem;
70
+ font-weight: 600;
71
+ transition: all 0.3s ease;
72
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
73
+ }
74
+ .stButton > button:hover {
75
+ transform: translateY(-2px);
76
+ box-shadow: 0 7px 20px rgba(102, 126, 234, 0.6);
77
+ }
78
+ .metric-card {
79
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
80
+ padding: 1.5rem;
81
+ border-radius: 15px;
82
+ color: white;
83
+ text-align: center;
84
+ box-shadow: 0 5px 20px rgba(102, 126, 234, 0.3);
85
+ margin-bottom: 1rem;
86
+ }
87
+ .feature-card {
88
+ background: #f8f9ff;
89
+ padding: 1.5rem;
90
+ border-radius: 15px;
91
+ border: 2px solid #e1e8ff;
92
+ margin-bottom: 1rem;
93
+ }
94
+ .cost-card {
95
+ background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
96
+ padding: 1.5rem;
97
+ border-radius: 15px;
98
+ color: white;
99
+ text-align: center;
100
+ box-shadow: 0 5px 20px rgba(17, 153, 142, 0.3);
101
+ margin-bottom: 1rem;
102
+ }
103
+ </style>
104
+ """, unsafe_allow_html=True)
105
+
106
+ # Header Section
107
+ st.markdown("""
108
+ <div class="header-container">
109
+ <h1 style="color: white; font-size: 3rem; margin-bottom: 0.5rem; text-shadow: 2px 2px 4px rgba(0,0,0,0.3);">🚀 PromptCraft AI</h1>
110
+ <h3 style="color: white; margin-top: 0; opacity: 0.9; font-weight: 300;">✨ Optimize Your AI Prompts, Save Money & Time ✨</h3>
111
+ <p style="color: white; opacity: 0.8; font-size: 1.1rem;">Transform verbose prompts into efficient, cost-effective versions without losing meaning</p>
112
+ </div>
113
+ """, unsafe_allow_html=True)
114
+
115
+ col1, col2 = st.columns([0.65, 0.35], gap="large")
116
+
117
+ with col1:
118
+ st.markdown("""
119
+ <div class="feature-card">
120
+ <h3 style="color: #667eea; margin-top: 0;">⚙️ Configuration</h3>
121
+ </div>
122
+ """, unsafe_allow_html=True)
123
+
124
+ st.markdown("**💰 LLM Cost Settings**")
125
+ model = st.selectbox("Select LLM Model", list(cost_model.keys()))
126
+
127
+ if model == "Custom":
128
+ input_cost = st.number_input("Input Cost ($/1K tokens)", 0.01, 1.0, 0.03)
129
+ output_cost = st.number_input("Output Cost ($/1K tokens)", 0.01, 1.0, 0.06)
130
+ else:
131
+ input_cost, output_cost = cost_model[model]
132
+
133
+ st.markdown("**🤖 Optimization Model**")
134
+
135
+ # Create columns for the optimizer section
136
+ opt_col1, opt_col2 = st.columns([1, 1])
137
+
138
+ with opt_col1:
139
+ optimizer_model = st.selectbox("Choose Optimizer", ["spaCy + Lemminflect", "GPT-5"])
140
+
141
+ persona = "Default"
142
+ api_key_input = ""
143
+ tavily_api_key_input = ""
144
+
145
+ if optimizer_model == "GPT-5":
146
+ with opt_col2:
147
+ persona = st.selectbox("Choose Persona", list(PERSONAS.keys()))
148
+
149
+ # API Keys in the same row
150
+ api_col1, api_col2 = st.columns([1, 1])
151
+ with api_col1:
152
+ api_key_input = st.text_input("AIMLAPI API Key (optional)", type="password", help="If you don't provide a key, the one in your .env file will be used.")
153
+ with api_col2:
154
+ tavily_api_key_input = st.text_input("Tavily API Key (optional)", type="password", help="If you don't provide a key, the one in your .env file will be used.")
155
+ elif optimizer_model == "spaCy + Lemminflect":
156
+ with opt_col2:
157
+ aggressiveness = st.slider(
158
+ "Optimization Level",
159
+ 0.0,
160
+ 1.0,
161
+ 0.7,
162
+ help="Higher = more aggressive shortening",
163
+ )
164
+ else:
165
+ aggressiveness = 1.0
166
+
167
+ st.markdown("**📝 Your Prompt**")
168
+ prompt = st.text_area(
169
+ "Original Prompt",
170
+ height=200,
171
+ placeholder="✨ Paste your AI prompt here and watch the magic happen...\n\nExample: 'Please analyze this data very carefully and provide a comprehensive detailed report with all the advantages and disadvantages'",
172
+ help="Enter the prompt you want to optimize. The optimizer will reduce token count while preserving meaning."
173
+ )
174
+
175
+ col_btn1, col_btn2, col_btn3 = st.columns([1, 2, 1])
176
+ with col_btn2:
177
+ optimize_clicked = st.button("🚀 Optimize My Prompt", type="primary", use_container_width=True)
178
+
179
+ if optimize_clicked:
180
+ if optimizer_model == "spaCy + Lemminflect":
181
+ optimizer = AdvancedPromptOptimizer()
182
+ optimized, orig_toks, new_toks = optimizer.optimize(prompt, aggressiveness)
183
+ else: # GPT-5
184
+ api_key = api_key_input if api_key_input else os.getenv("AIMLAPI_API_KEY")
185
+ tavily_api_key = tavily_api_key_input if tavily_api_key_input else os.getenv("TAVILY_API_KEY")
186
+ if not api_key or api_key == "<YOUR_API_KEY>":
187
+ st.error("Please set your AIMLAPI_API_KEY in the .env file or enter it above.")
188
+ return
189
+ optimized = optimize_with_llm(prompt, api_key, persona, tavily_api_key=tavily_api_key)
190
+ # We need to calculate the tokens for the optimized prompt
191
+ # This is a simplification, as we don't have the exact tokenizer for gpt-5
192
+ # We will use tiktoken as an approximation
193
+ import tiktoken
194
+ tokenizer = tiktoken.get_encoding("cl100k_base")
195
+ orig_toks = len(tokenizer.encode(prompt))
196
+ new_toks = len(tokenizer.encode(optimized))
197
+
198
+ if orig_toks == 0:
199
+ st.warning("Please enter a valid prompt.")
200
+ return
201
+
202
+ # Calculate savings
203
+ token_savings = orig_toks - new_toks
204
+ percent_savings = (token_savings / orig_toks) * 100 if orig_toks > 0 else 0
205
+ input_cost_savings = token_savings * input_cost / 1000
206
+ output_cost_savings = token_savings * output_cost / 1000
207
+ total_cost_savings = input_cost_savings + output_cost_savings
208
+
209
+ with col1:
210
+ st.markdown("""
211
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 1rem; border-radius: 15px; margin-bottom: 1rem;">
212
+ <h3 style="color: white; text-align: center; margin: 0;">✨ Optimized Prompt</h3>
213
+ </div>
214
+ """, unsafe_allow_html=True)
215
+
216
+ st.code(optimized, language="text")
217
+
218
+ # Enhanced download button
219
+ col_dl1, col_dl2, col_dl3 = st.columns([1, 2, 1])
220
+ with col_dl2:
221
+ st.download_button(
222
+ "📥 Download Optimized Prompt",
223
+ optimized,
224
+ file_name="optimized_prompt.txt",
225
+ use_container_width=True
226
+ )
227
+
228
+ with col2:
229
+ st.markdown("""
230
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 1rem; border-radius: 15px; margin-bottom: 1rem;">
231
+ <h3 style="color: white; text-align: center; margin: 0;">📊 Optimization Results</h3>
232
+ </div>
233
+ """, unsafe_allow_html=True)
234
+
235
+ # Token Savings Card
236
+ st.markdown(
237
+ f"""
238
+ <div class="metric-card">
239
+ <h4 style="margin-top:0; opacity: 0.9;">🎯 Token Reduction</h4>
240
+ <div style="font-size:36px;font-weight:bold;margin:10px 0;">
241
+ {percent_savings:.1f}%
242
+ </div>
243
+ <div style="opacity: 0.8; font-size:16px;">
244
+ {token_savings} tokens saved
245
+ </div>
246
+ </div>
247
+ """,
248
+ unsafe_allow_html=True,
249
+ )
250
+
251
+ # Cost Savings Card
252
+ if orig_toks > 0 and (input_cost + output_cost) > 0:
253
+ cost_percent_savings = (
254
+ total_cost_savings
255
+ / (orig_toks * (input_cost + output_cost) / 1000)
256
+ * 100
257
+ )
258
+ else:
259
+ cost_percent_savings = 0
260
+ st.markdown(
261
+ f"""
262
+ <div class="cost-card">
263
+ <h4 style="margin-top:0; opacity: 0.9;">💸 Cost Reduction</h4>
264
+ <div style="font-size:36px;font-weight:bold;margin:10px 0;">
265
+ {cost_percent_savings:.1f}%
266
+ </div>
267
+ <div style="opacity: 0.8; font-size:16px;">
268
+ ${total_cost_savings:.4f} saved per call
269
+ </div>
270
+ </div>
271
+ """,
272
+ unsafe_allow_html=True,
273
+ )
274
+
275
+ # Visual Progress Indicator
276
+ progress_value = min(1.0, max(0.0, percent_savings / 100))
277
+ st.markdown("**📈 Optimization Progress**")
278
+ st.progress(progress_value)
279
+ st.markdown(f"<p style='text-align: center; color: #667eea; font-weight: 500;'>Prompt reduced to {100-percent_savings:.1f}% of original size</p>", unsafe_allow_html=True)
280
+
281
+ # Detailed Breakdown
282
+ with st.expander("📊 Cost Analysis"):
283
+ col_a, col_b = st.columns(2)
284
+ with col_a:
285
+ st.markdown(
286
+ f"**Input Cost**\n\n"
287
+ f"Original: {format_cost(orig_toks, input_cost)}\n\n"
288
+ f"Optimized: {format_cost(new_toks, input_cost)}\n\n"
289
+ f"Saved: {format_cost(token_savings, input_cost)}"
290
+ )
291
+ with col_b:
292
+ st.markdown(
293
+ f"**Output Cost**\n\n"
294
+ f"Original: {format_cost(orig_toks, output_cost)}\n\n"
295
+ f"Optimized: {format_cost(new_toks, output_cost)}\n\n"
296
+ f"Saved: {format_cost(token_savings, output_cost)}"
297
+ )
298
+
299
+ # Optimization report
300
+ with st.expander("🔍 Applied Optimizations"):
301
+ st.markdown("### Common Transformations")
302
+ st.json(
303
+ {
304
+ "Removed fillers": "e.g., 'very', 'carefully'",
305
+ "Shortened phrases": "'advantages/disadvantages' → 'pros/cons'",
306
+ "Structural changes": "Simplified JSON formatting",
307
+ "Verb optimization": "Converted to base forms",
308
+ "Preposition removal": "Dropped non-essential connectors",
309
+ }
310
+ )
311
+
312
+ st.markdown("### Share Your Savings")
313
+ st.code(
314
+ f"Saved {token_savings} tokens (${total_cost_savings:.4f}) with #PromptOptimizer\n"
315
+ f"Optimization level: {aggressiveness*100:.0f}%"
316
+ )
317
+
318
+ if __name__ == "__main__":
319
+ main()
src/assets/01.png ADDED

Git LFS Details

  • SHA256: 7726e45ceeae59a9793f43212c11cd8f5b195e5273829683d0ada4d4f42cc890
  • Pointer size: 131 Bytes
  • Size of remote file: 115 kB
src/assets/02.png ADDED

Git LFS Details

  • SHA256: 0465e50006f8ec38c2af7be95948e8aba63c8df9817c1b2652d1913a5af32104
  • Pointer size: 132 Bytes
  • Size of remote file: 1.47 MB
src/assets/detailed_results.png ADDED

Git LFS Details

  • SHA256: 7bcfe6166f4d55910ab77437de07ff168381b0668eea7c68f087ddedbc951822
  • Pointer size: 131 Bytes
  • Size of remote file: 112 kB
src/assets/main_ui.png ADDED

Git LFS Details

  • SHA256: d6510b2ea4c392b66b152621f0437e77158cc66c9e9f758c1b3316e1f9326135
  • Pointer size: 130 Bytes
  • Size of remote file: 71 kB
src/engine.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import spacy
3
+ import tiktoken
4
+ from lemminflect import getLemma
5
+
6
+ class AdvancedPromptOptimizer:
7
+ def __init__(self):
8
+ # For NER, consider using en_core_web_md for better accuracy
9
+ self.nlp = spacy.load("en_core_web_sm")
10
+ self.nlp.Defaults.stop_words -= {"not", "no", "never"}
11
+ self.tokenizer = tiktoken.get_encoding("cl100k_base")
12
+ self.negation_words = {"not", "no", "never", "without", "except"}
13
+
14
+ def _mask_spans(self, s):
15
+ masks = {}
16
+ # triple backticks
17
+ s, n = re.subn(r"```.*?```", lambda m: masks.setdefault(f"<CODE{len(masks)}>", m.group(0)) or list(masks.keys())[-1], s, flags=re.S)
18
+ # inline code
19
+ s = re.sub(r"`[^`]+`", lambda m: masks.setdefault(f"<IC{len(masks)}>", m.group(0)) or list(masks.keys())[-1], s)
20
+ # urls
21
+ s = re.sub(r"https?://\S+", lambda m: masks.setdefault(f"<URL{len(masks)}>", m.group(0)) or list(masks.keys())[-1], s)
22
+ # comparators
23
+ s = re.sub(r"\b(less than|at least|no more than)\b", lambda m: masks.setdefault(f"<CMP{len(masks)}>", m.group(0)) or list(masks.keys())[-1], s, flags=re.I)
24
+ return s, masks
25
+
26
+ def _unmask_spans(self, s, masks):
27
+ for k, v in masks.items():
28
+ s = s.replace(k, v)
29
+ return s
30
+
31
+ def optimize(self, prompt: str, aggressiveness: float = 0.7) -> tuple:
32
+ """Optimize prompt with token counting"""
33
+ masked_prompt, masks = self._mask_spans(prompt)
34
+ optimized = self._apply_rules(masked_prompt, aggressiveness)
35
+ optimized = self._linguistic_optimize(optimized, aggressiveness)
36
+ optimized = self._unmask_spans(optimized, masks)
37
+ optimized = re.sub(r"\s+", " ", optimized).strip()
38
+
39
+ try:
40
+ orig_tokens = len(self.tokenizer.encode(prompt))
41
+ new_tokens = len(self.tokenizer.encode(optimized))
42
+ except:
43
+ orig_tokens = len(prompt.split())
44
+ new_tokens = len(optimized.split())
45
+
46
+ return optimized, orig_tokens, new_tokens
47
+
48
+ def _apply_rules(self, text: str, aggressiveness: float) -> str:
49
+ # Apply safer rules first
50
+ rules = [
51
+ (r"\s{2,}", " ", 0.0),
52
+ (r"\b(\w+)\s+\1\b", r"\1", 0.0),
53
+ (r"\b(advantages and disadvantages)\b", "pros/cons", 0.5),
54
+ (r"\b(in a detailed manner|in a detailed way)\b", "", 0.7),
55
+ (r"\b(I want to|I need to|I would like to)\b", "", 0.7),
56
+ (r"\b(for example|e\.g\.|such as|i\.e\.)\b", "e.g.", 0.8),
57
+ (r"\b(please\s+)?(kindly\s+)?(carefully|very|extremely|really|quite)\b", "", 0.8),
58
+ (r"\b(can you|could you|would you)\b", "", 0.9),
59
+ (r"\b(output|provide|give|return)\s+in\s+(JSON|json)\s+format\b", "JSON:", 1.0),
60
+ ]
61
+ for pattern, repl, priority in rules:
62
+ if aggressiveness >= priority:
63
+ text = re.sub(pattern, repl, text, flags=re.IGNORECASE)
64
+ return text
65
+
66
+ def _linguistic_optimize(self, text: str, aggressiveness: float) -> str:
67
+ if not text.strip():
68
+ return text
69
+ doc = self.nlp(text)
70
+ out = []
71
+ for token in doc:
72
+ # Guard important labels
73
+ if token.text.lower() in ["deliverables:", "constraints:", "metrics:"] and token.is_sent_start:
74
+ out.append(token.text)
75
+ continue
76
+
77
+ if token.pos_ in ("PUNCT", "SPACE"): continue
78
+ if token.like_num or token.ent_type_ or token.dep_ == "neg" or token.text.lower() in self.negation_words:
79
+ out.append(token.text)
80
+ continue
81
+ if token.pos_ in ("PROPN", "NUM", "NOUN", "ADJ"):
82
+ out.append(token.text)
83
+ continue
84
+ if token.pos_ == "VERB":
85
+ if aggressiveness >= 0.8:
86
+ lemma = getLemma(token.text, upos="VERB") or [token.lemma_]
87
+ out.append(lemma[0])
88
+ else:
89
+ out.append(token.text)
90
+ continue
91
+ if token.pos_ in ("ADV", "DET", "PRON"):
92
+ if aggressiveness < 0.6:
93
+ out.append(token.text)
94
+ # else drop
95
+ continue
96
+ out.append(token.text)
97
+ return " ".join(out)
src/llm_optimizer.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tiktoken
3
+ from langchain_openai import ChatOpenAI
4
+ from langchain_core.prompts import ChatPromptTemplate
5
+ from langchain_tavily import TavilySearch
6
+ from langgraph.prebuilt import create_react_agent
7
+
8
+ PERSONAS = {
9
+ "Default": "<role>You are a helpful assistant that optimizes prompts for clarity, conciseness, and effectiveness.</role>\n<task>Your goal is to reduce token count while preserving the original intent.</task>\n<rules>\n1. Remove filler words.\n2. Simplify complex sentences.\n3. Use active voice.\n4. Convert jargon to plain language.\n</rules>",
10
+ "UI/UX Designer": "<role>You are a UI/UX designer.</role>\n<task>Optimize the following prompt to be user-centric, clear, and focused on usability. Remove ambiguity and ensure the prompt is easy to understand for a general audience.</task>\n<example>\nUser: I want to find the settings page.\nAssistant: Locate settings page.\n</example>",
11
+ "Software Engineer": "<role>You are a software engineer.</role>\n<task>Optimize the following prompt for technical accuracy, conciseness, and clarity. Remove any non-essential information and focus on the core technical request.</task>\n<example>\nUser: The app is crashing when I click the button.\nAssistant: Debug app crash on button click.\n</example>",
12
+ "Marketing Copywriter": "<role>You are a marketing copywriter.</role>\n<task>Optimize the following prompt to be persuasive, engaging, and impactful. Focus on action-oriented language and a clear call to action.</task>\n<example>\nUser: Tell me about your new product.\nAssistant: Describe the new product's features and benefits.\n</example>",
13
+ "Creative Writer": "<role>You are a creative writer.</role>\n<task>Optimize the following prompt to be imaginative, descriptive, and evocative. Enhance the original prompt to inspire a more creative response.</task>\n<example>\nUser: Write a story about a cat.\nAssistant: Write a story about a mischievous cat who goes on an adventure.\n</example>",
14
+ "Technical Writer": "<role>You are a technical writer.</role>\n<task>Optimize the following prompt to be clear, concise, and easy to follow. Use simple language and avoid jargon.</task>\n<example>\nUser: How do I install the software?\nAssistant: Provide step-by-step instructions for installing the software.\n</example>",
15
+ "Legal Advisor": "<role>You are a legal advisor.</role>\n<task>Optimize the following prompt for legal accuracy and clarity. Ensure the prompt is unambiguous and does not contain any misleading information.</task>\n<example>\nUser: What are the terms of the contract?\nAssistant: Summarize the key terms and conditions of the contract.\n</example>",
16
+ "Medical Professional": "<role>You are a medical professional.</role>\n<task>Optimize the following prompt for medical accuracy and clarity. Use precise medical terminology and avoid generalizations.</task>\n<example>\nUser: What are the symptoms of the flu?\nAssistant: List the common symptoms of influenza.\n</example>",
17
+ "Financial Analyst": "<role>You are a financial analyst.</role>\n<task>Optimize the following prompt for financial accuracy and clarity. Use precise financial terminology and avoid speculation.</task>\n<example>\nUser: What is the company's financial performance?\nAssistant: Analyze the company's key financial metrics from the latest earnings report.\n</example>",
18
+ }
19
+
20
+ # ============================================================================
21
+ # SIMPLE LLM OPTIMIZATION (No Search)
22
+ # ============================================================================
23
+
24
+ def optimize_with_llm(prompt: str, api_key: str, persona: str = "Default") -> str:
25
+ """Simple LLM-based prompt optimization without search enhancement."""
26
+ chat = ChatOpenAI(
27
+ base_url="https://api.aimlapi.com/v1",
28
+ api_key=api_key,
29
+ model="openai/gpt-5-chat-latest",
30
+ temperature=0,
31
+ )
32
+
33
+ system_prompt = PERSONAS.get(persona, PERSONAS["Default"])
34
+ system_prompt += "\n\nIMPORTANT: Return ONLY the optimized prompt without any explanations, prefixes, markdown formatting, or additional text."
35
+
36
+ prompt_template = ChatPromptTemplate.from_messages([
37
+ ("system", system_prompt),
38
+ ("human", "Shorten and optimize the following prompt: {prompt}"),
39
+ ])
40
+
41
+ chain = prompt_template | chat
42
+ response = chain.invoke({"prompt": prompt})
43
+ return response.content
44
+
45
+ # ============================================================================
46
+ # AGENT-BASED OPTIMIZATION WITH SEARCH ENHANCEMENT
47
+ # ============================================================================
48
+
49
+ def get_accurate_token_count(text: str, model_name: str = "gpt-4") -> int:
50
+ """Get accurate token count using model-specific tokenizer."""
51
+ try:
52
+ if "gpt-5" in model_name.lower() or "gpt-4" in model_name.lower():
53
+ enc = tiktoken.get_encoding("o200k_base") # Latest encoding
54
+ else:
55
+ enc = tiktoken.encoding_for_model("gpt-4") # Fallback
56
+ return len(enc.encode(text))
57
+ except Exception:
58
+ enc = tiktoken.get_encoding("cl100k_base")
59
+ return len(enc.encode(text))
60
+
61
+ def create_prompt_enhancement_agent(api_key: str, tavily_api_key: str, persona: str = "Default"):
62
+ """Create a ReAct agent with persona-specific system prompt."""
63
+ if tavily_api_key:
64
+ os.environ["TAVILY_API_KEY"] = tavily_api_key
65
+
66
+ # Get persona-specific instructions
67
+ persona_prompt = PERSONAS.get(persona, PERSONAS["Default"])
68
+
69
+ # Create comprehensive system prompt that includes persona details
70
+ system_prompt = f"""You are an intelligent prompt enhancement and optimization agent.
71
+
72
+ {persona_prompt}
73
+
74
+ WORKFLOW:
75
+ 1. First, analyze the user's prompt to determine if it needs current information
76
+ 2. If current information would improve the prompt, use the tavily_search tool to find relevant, up-to-date data
77
+ 3. Enhance the original prompt by incorporating valuable current information (if found)
78
+ 4. Apply your persona-specific optimization approach to make the prompt clear and concise
79
+ 5. Return ONLY the final optimized prompt - no explanations or metadata
80
+
81
+ SEARCH GUIDELINES:
82
+ - Only search when the prompt would genuinely benefit from current information
83
+ - Look for keywords like "latest", "current", "2024", "2025", "recent", "new"
84
+ - Consider if your persona typically needs up-to-date information
85
+ - Use 3-6 relevant search terms
86
+ - Evaluate search results for relevance before incorporating
87
+
88
+ OPTIMIZATION PRINCIPLES:
89
+ - Preserve the user's original intent
90
+ - Apply persona-specific expertise
91
+ - Balance enhancement with conciseness
92
+ - Use clear, actionable language
93
+ - Remove unnecessary words while maintaining meaning"""
94
+
95
+ # Set up LLM
96
+ llm = ChatOpenAI(
97
+ base_url="https://api.aimlapi.com/v1",
98
+ api_key=api_key,
99
+ model="openai/gpt-5-chat-latest",
100
+ temperature=0.1
101
+ )
102
+
103
+ # Configure search tool
104
+ tavily_search_tool = TavilySearch(
105
+ max_results=3,
106
+ topic="general",
107
+ include_answer=True,
108
+ search_depth="basic"
109
+ )
110
+
111
+ # Create ReAct agent with system prompt
112
+ try:
113
+ agent = create_react_agent(
114
+ llm,
115
+ [tavily_search_tool],
116
+ state_modifier=system_prompt
117
+ )
118
+ except TypeError:
119
+ # Fallback if state_modifier is not supported
120
+ agent = create_react_agent(llm, [tavily_search_tool])
121
+ # Store system prompt for use in message
122
+ agent._system_prompt = system_prompt
123
+
124
+ return agent
125
+
126
+ def enhance_and_optimize_prompt(prompt: str, persona: str, agent):
127
+ """Use the agent to enhance and optimize the prompt."""
128
+
129
+ # Check if we stored a system prompt (fallback case)
130
+ if hasattr(agent, '_system_prompt'):
131
+ # Include system prompt in the user message
132
+ user_instruction = f"""{agent._system_prompt}
133
+
134
+ Original prompt to enhance and optimize: {prompt}"""
135
+ else:
136
+ # Simple instruction if system prompt was set via state_modifier
137
+ user_instruction = f"Enhance and optimize this prompt: {prompt}"
138
+
139
+ # Agent handles the entire flow autonomously
140
+ result = agent.invoke({"messages": [{"role": "user", "content": user_instruction}]})
141
+
142
+ return result["messages"][-1].content
143
+
144
+ def optimize_with_agent(prompt: str, api_key: str, persona: str = "Default", tavily_api_key: str = None) -> str:
145
+ """Agent-based prompt optimization with search enhancement."""
146
+ agent = create_prompt_enhancement_agent(api_key, tavily_api_key, persona)
147
+ optimized_prompt = enhance_and_optimize_prompt(prompt, persona, agent)
148
+
149
+ return optimized_prompt
150
+
151
+ # ============================================================================
152
+ # UTILITY FUNCTIONS
153
+ # ============================================================================
154
+
155
+ def compare_optimization_methods(prompt: str, api_key: str, persona: str = "Default", tavily_api_key: str = None) -> dict:
156
+ """Compare simple LLM optimization vs agent-based optimization."""
157
+
158
+ # Simple optimization
159
+ simple_result = optimize_with_llm(prompt, api_key, persona)
160
+ simple_tokens = get_accurate_token_count(simple_result)
161
+
162
+ # Agent optimization (if tavily_api_key provided)
163
+ if tavily_api_key:
164
+ agent_result = optimize_with_agent(prompt, api_key, persona, tavily_api_key)
165
+ agent_tokens = get_accurate_token_count(agent_result)
166
+ else:
167
+ agent_result = "N/A - Tavily API key required"
168
+ agent_tokens = 0
169
+
170
+ # Original metrics
171
+ original_tokens = get_accurate_token_count(prompt)
172
+
173
+ return {
174
+ "original": {
175
+ "prompt": prompt,
176
+ "tokens": original_tokens
177
+ },
178
+ "simple_optimization": {
179
+ "prompt": simple_result,
180
+ "tokens": simple_tokens,
181
+ "reduction": original_tokens - simple_tokens
182
+ },
183
+ "agent_optimization": {
184
+ "prompt": agent_result,
185
+ "tokens": agent_tokens,
186
+ "reduction": original_tokens - agent_tokens if agent_tokens > 0 else "N/A"
187
+ },
188
+ "persona": persona
189
+ }
190
+
191
+ def batch_optimize(prompts: list, api_key: str, persona: str = "Default", method: str = "simple") -> list:
192
+ """Optimize multiple prompts using the specified method."""
193
+ results = []
194
+
195
+ for prompt in prompts:
196
+ if method == "simple":
197
+ optimized = optimize_with_llm(prompt, api_key, persona)
198
+ elif method == "agent":
199
+ # Note: This would require tavily_api_key for agent method
200
+ optimized = "Agent method requires tavily_api_key parameter"
201
+ else:
202
+ optimized = "Invalid method. Use 'simple' or 'agent'"
203
+
204
+ results.append({
205
+ "original": prompt,
206
+ "optimized": optimized,
207
+ "persona": persona,
208
+ "method": method
209
+ })
210
+
211
+ return results
src/main.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ def main():
2
+ print("Hello from ai-prompt-optimizer!")
3
+
4
+
5
+ if __name__ == "__main__":
6
+ main()
src/mcp/mcp_server.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
4
+
5
+ import spacy
6
+ import tiktoken
7
+ from lemminflect import getLemma
8
+ import re
9
+ from llm_optimizer import (
10
+ optimize_with_llm,
11
+ optimize_with_agent,
12
+ get_accurate_token_count,
13
+ PERSONAS
14
+ )
15
+ #from fastmcp import FastMCP
16
+ #from mcp de claude
17
+ from mcp.server.fastmcp import FastMCP
18
+ from mcp.server.fastmcp.prompts import base
19
+ from starlette.middleware import Middleware
20
+ from starlette.middleware.cors import CORSMiddleware
21
+ from pydantic import Field
22
+
23
+ # ============================================================================
24
+ # spaCy Optimizer (kept separate as it's unique to this implementation)
25
+ # ============================================================================
26
+
27
+ class AdvancedPromptOptimizer:
28
+ def __init__(self):
29
+ self.nlp = spacy.load("en_core_web_sm")
30
+ self.nlp.Defaults.stop_words -= {"not", "no", "never"}
31
+ self.tokenizer = tiktoken.get_encoding("cl100k_base")
32
+ self.negation_words = {"not", "no", "never", "without", "except"}
33
+
34
+ def _mask_spans(self, s):
35
+ masks = {}
36
+ # triple backticks
37
+ s, n = re.subn(r"```.*?```", lambda m: masks.setdefault(f"<CODE{len(masks)}>", m.group(0)) or list(masks.keys())[-1], s, flags=re.S)
38
+ # inline code
39
+ s = re.sub(r"`[^`]+`", lambda m: masks.setdefault(f"<IC{len(masks)}>", m.group(0)) or list(masks.keys())[-1], s)
40
+ # urls
41
+ s = re.sub(r"https?://\S+", lambda m: masks.setdefault(f"<URL{len(masks)}>", m.group(0)) or list(masks.keys())[-1], s)
42
+ # comparators
43
+ s = re.sub(r"\b(less than|at least|no more than)\b", lambda m: masks.setdefault(f"<CMP{len(masks)}>", m.group(0)) or list(masks.keys())[-1], s, flags=re.I)
44
+ return s, masks
45
+
46
+ def _unmask_spans(self, s, masks):
47
+ for k, v in masks.items():
48
+ s = s.replace(k, v)
49
+ return s
50
+
51
+ def optimize(self, prompt: str, aggressiveness: float = 0.7) -> tuple:
52
+ """Optimize prompt with token counting"""
53
+ masked_prompt, masks = self._mask_spans(prompt)
54
+ optimized = self._apply_rules(masked_prompt, aggressiveness)
55
+ optimized = self._linguistic_optimize(optimized, aggressiveness)
56
+ optimized = self._unmask_spans(optimized, masks)
57
+ optimized = re.sub(r"\s+", " ", optimized).strip()
58
+
59
+ try:
60
+ orig_tokens = len(self.tokenizer.encode(prompt))
61
+ new_tokens = len(self.tokenizer.encode(optimized))
62
+ except:
63
+ orig_tokens = len(prompt.split())
64
+ new_tokens = len(optimized.split())
65
+
66
+ return optimized, orig_tokens, new_tokens
67
+
68
+ def _apply_rules(self, text: str, aggressiveness: float) -> str:
69
+ rules = [
70
+ (r"\s{2,}", " ", 0.0),
71
+ (r"\b(\w+)\s+\1\b", r"\1", 0.0),
72
+ (r"\b(advantages and disadvantages)\b", "pros/cons", 0.5),
73
+ (r"\b(in a detailed manner|in a detailed way)\b", "", 0.7),
74
+ (r"\b(I want to|I need to|I would like to)\b", "", 0.7),
75
+ (r"\b(for example|e\.g\.|such as|i\.e\.)\b", "e.g.", 0.8),
76
+ (r"\b(please\s+)?(kindly\s+)?(carefully|very|extremely|really|quite)\b", "", 0.8),
77
+ (r"\b(can you|could you|would you)\b", "", 0.9),
78
+ (r"\b(output|provide|give|return)\s+in\s+(JSON|json)\s+format\b", "JSON:", 1.0),
79
+ ]
80
+ for pattern, repl, priority in rules:
81
+ if aggressiveness >= priority:
82
+ text = re.sub(pattern, repl, text, flags=re.IGNORECASE)
83
+ return text
84
+
85
+ def _linguistic_optimize(self, text: str, aggressiveness: float) -> str:
86
+ if not text.strip():
87
+ return text
88
+ doc = self.nlp(text)
89
+ out = []
90
+ for token in doc:
91
+ if token.text.lower() in ["deliverables:", "constraints:", "metrics:"] and token.is_sent_start:
92
+ out.append(token.text)
93
+ continue
94
+
95
+ if token.pos_ in ("PUNCT", "SPACE"): continue
96
+ if token.like_num or token.ent_type_ or token.dep_ == "neg" or token.text.lower() in self.negation_words:
97
+ out.append(token.text)
98
+ continue
99
+ if token.pos_ in ("PROPN", "NUM", "NOUN", "ADJ"):
100
+ out.append(token.text)
101
+ continue
102
+ if token.pos_ == "VERB":
103
+ if aggressiveness >= 0.8:
104
+ lemma = getLemma(token.text, upos="VERB") or [token.lemma_]
105
+ out.append(lemma[0])
106
+ else:
107
+ out.append(token.text)
108
+ continue
109
+ if token.pos_ in ("ADV", "DET", "PRON"):
110
+ if aggressiveness < 0.6:
111
+ out.append(token.text)
112
+ continue
113
+ out.append(token.text)
114
+ return " ".join(out)
115
+
116
+ # ============================================================================
117
+ # MCP Server
118
+ # ============================================================================
119
+
120
+ mcp = FastMCP(name="PromptOptimizer", log_level="ERROR")
121
+
122
+ @mcp.tool(
123
+ name="optimize_prompt",
124
+ description="Optimizes a given prompt using various methods to reduce token count while preserving meaning.",
125
+ )
126
+ def optimize_prompt(
127
+ prompt: str = Field(description="The prompt to optimize"),
128
+ method: str = Field(default="simple", description="The optimization method to use. Can be 'simple', 'agent', or 'spacy'"),
129
+ persona: str = Field(default="Default", description="The persona to use for LLM-based optimization"),
130
+ api_key: str = Field(default=None, description="The API key for the LLM"),
131
+ tavily_api_key: str = Field(default=None, description="The API key for Tavily search"),
132
+ aggressiveness: float = Field(default=0.7, description="The aggressiveness level for spaCy-based optimization"),
133
+ ) -> str:
134
+ if method == "simple":
135
+ if not api_key:
136
+ return "API key is required for simple optimization."
137
+ return optimize_with_llm(prompt, api_key, persona)
138
+ elif method == "agent":
139
+ if not api_key or not tavily_api_key:
140
+ return "API key and Tavily API key are required for agent-based optimization."
141
+ return optimize_with_agent(prompt, api_key, persona, tavily_api_key)
142
+ elif method == "spacy":
143
+ optimizer = AdvancedPromptOptimizer()
144
+ optimized, _, _ = optimizer.optimize(prompt, aggressiveness)
145
+ return optimized
146
+ else:
147
+ return "Invalid method. Use 'simple', 'agent', or 'spacy'."
148
+
149
+ @mcp.tool(
150
+ name="get_available_personas",
151
+ description="Get list of available optimization personas and their descriptions.",
152
+ )
153
+ def get_available_personas() -> str:
154
+ return "\n".join([f"- {persona}: {desc.split('.')[0]}..." for persona, desc in PERSONAS.items()])
155
+
156
+ @mcp.tool(
157
+ name="count_tokens",
158
+ description="Count tokens in text using specified model tokenizer.",
159
+ )
160
+ def count_tokens(
161
+ text: str = Field(description="The text to count tokens for"),
162
+ model: str = Field(default="gpt-4", description="The model to use for tokenization"),
163
+ ) -> str:
164
+ count = get_accurate_token_count(text, model)
165
+ return f"Token count: {count}"
166
+
167
+
168
+ @mcp.resource("prompts://optimization", mime_type="application/json")
169
+ def list_optimization_methods() -> list[str]:
170
+ return ["simple", "agent", "spacy"]
171
+
172
+
173
+ @mcp.resource("prompts://personas", mime_type="application/json")
174
+ def list_personas() -> list[str]:
175
+ return list(PERSONAS.keys())
176
+
177
+
178
+ @mcp.resource("prompts://personas/{persona_id}", mime_type="text/plain")
179
+ def fetch_persona_details(persona_id: str) -> str:
180
+ if persona_id not in PERSONAS:
181
+ raise ValueError(f"Persona with id {persona_id} not found")
182
+ return PERSONAS[persona_id]
183
+
184
+
185
+ @mcp.prompt(
186
+ name="optimize_for_persona",
187
+ description="Creates an optimization prompt tailored to a specific persona.",
188
+ )
189
+ def optimize_for_persona(
190
+ text: str = Field(description="Text to optimize"),
191
+ persona: str = Field(description="Persona to optimize for"),
192
+ ) -> list[base.Message]:
193
+ if persona not in PERSONAS:
194
+ persona = "Default"
195
+
196
+ prompt = f"""
197
+ Your goal is to optimize the following text for the {persona} persona.
198
+
199
+ The text to optimize is:
200
+ <text>
201
+ {text}
202
+ </text>
203
+
204
+ Persona guidelines:
205
+ {PERSONAS[persona]}
206
+
207
+ Use the 'optimize_prompt' tool with method='simple' to optimize the text.
208
+ After optimization, respond with the optimized version and explain what changes were made.
209
+ """
210
+
211
+ return [base.UserMessage(prompt)]
212
+
213
+ if __name__ == "__main__":
214
+ #mcp.run(transport="http", host="127.0.0.1", port=8000, path="/mcp")
215
+ mcp.run(transport="streamable-http")
src/mcp/mcp_server_fastmcp.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
4
+
5
+ import spacy
6
+ import tiktoken
7
+ from lemminflect import getLemma
8
+ import re
9
+ from llm_optimizer import (
10
+ optimize_with_llm,
11
+ optimize_with_agent,
12
+ get_accurate_token_count,
13
+ PERSONAS
14
+ )
15
+ from fastmcp import FastMCP
16
+ from pydantic import Field
17
+ from dotenv import load_dotenv
18
+
19
+ # Load environment variables from local .env file
20
+ load_dotenv()
21
+
22
+ # ============================================================================
23
+ # spaCy Optimizer (kept separate as it's unique to this implementation)
24
+ # ============================================================================
25
+
26
+ class AdvancedPromptOptimizer:
27
+ def __init__(self):
28
+ self.nlp = spacy.load("en_core_web_sm")
29
+ self.nlp.Defaults.stop_words -= {"not", "no", "never"}
30
+ self.tokenizer = tiktoken.get_encoding("cl100k_base")
31
+ self.negation_words = {"not", "no", "never", "without", "except"}
32
+
33
+ def _mask_spans(self, s):
34
+ masks = {}
35
+ # triple backticks
36
+ s, _ = re.subn(r"```.*?```", lambda m: masks.setdefault(f"<CODE{len(masks)}>", m.group(0)) or list(masks.keys())[-1], s, flags=re.S)
37
+ # inline code
38
+ s = re.sub(r"`[^`]+`", lambda m: masks.setdefault(f"<IC{len(masks)}>", m.group(0)) or list(masks.keys())[-1], s)
39
+ # urls
40
+ s = re.sub(r"https?://\S+", lambda m: masks.setdefault(f"<URL{len(masks)}>", m.group(0)) or list(masks.keys())[-1], s)
41
+ # comparators
42
+ s = re.sub(r"\b(less than|at least|no more than)\b", lambda m: masks.setdefault(f"<CMP{len(masks)}>", m.group(0)) or list(masks.keys())[-1], s, flags=re.I)
43
+ return s, masks
44
+
45
+ def _unmask_spans(self, s, masks):
46
+ for k, v in masks.items():
47
+ s = s.replace(k, v)
48
+ return s
49
+
50
+ def optimize(self, prompt: str, aggressiveness: float = 0.7) -> tuple:
51
+ """Optimize prompt with token counting"""
52
+ masked_prompt, masks = self._mask_spans(prompt)
53
+ optimized = self._apply_rules(masked_prompt, aggressiveness)
54
+ optimized = self._linguistic_optimize(optimized, aggressiveness)
55
+ optimized = self._unmask_spans(optimized, masks)
56
+ optimized = re.sub(r"\s+", " ", optimized).strip()
57
+
58
+ try:
59
+ orig_tokens = len(self.tokenizer.encode(prompt))
60
+ new_tokens = len(self.tokenizer.encode(optimized))
61
+ except:
62
+ orig_tokens = len(prompt.split())
63
+ new_tokens = len(optimized.split())
64
+
65
+ return optimized, orig_tokens, new_tokens
66
+
67
+ def _apply_rules(self, text: str, aggressiveness: float) -> str:
68
+ rules = [
69
+ (r"\s{2,}", " ", 0.0),
70
+ (r"\b(\w+)\s+\1\b", r"\1", 0.0),
71
+ (r"\b(advantages and disadvantages)\b", "pros/cons", 0.5),
72
+ (r"\b(in a detailed manner|in a detailed way)\b", "", 0.7),
73
+ (r"\b(I want to|I need to|I would like to)\b", "", 0.7),
74
+ (r"\b(for example|e\.g\.|such as|i\.e\.)\b", "e.g.", 0.8),
75
+ (r"\b(please\s+)?(kindly\s+)?(carefully|very|extremely|really|quite)\b", "", 0.8),
76
+ (r"\b(can you|could you|would you)\b", "", 0.9),
77
+ (r"\b(output|provide|give|return)\s+in\s+(JSON|json)\s+format\b", "JSON:", 1.0),
78
+ ]
79
+ for pattern, repl, priority in rules:
80
+ if aggressiveness >= priority:
81
+ text = re.sub(pattern, repl, text, flags=re.IGNORECASE)
82
+ return text
83
+
84
+ def _linguistic_optimize(self, text: str, aggressiveness: float) -> str:
85
+ if not text.strip():
86
+ return text
87
+ doc = self.nlp(text)
88
+ out = []
89
+ for token in doc:
90
+ if token.text.lower() in ["deliverables:", "constraints:", "metrics:"] and token.is_sent_start:
91
+ out.append(token.text)
92
+ continue
93
+
94
+ if token.pos_ in ("PUNCT", "SPACE"): continue
95
+ if token.like_num or token.ent_type_ or token.dep_ == "neg" or token.text.lower() in self.negation_words:
96
+ out.append(token.text)
97
+ continue
98
+ if token.pos_ in ("PROPN", "NUM", "NOUN", "ADJ"):
99
+ out.append(token.text)
100
+ continue
101
+ if token.pos_ == "VERB":
102
+ if aggressiveness >= 0.8:
103
+ lemma = getLemma(token.text, upos="VERB") or [token.lemma_]
104
+ out.append(lemma[0])
105
+ else:
106
+ out.append(token.text)
107
+ continue
108
+ if token.pos_ in ("ADV", "DET", "PRON"):
109
+ if aggressiveness < 0.6:
110
+ out.append(token.text)
111
+ continue
112
+ out.append(token.text)
113
+ return " ".join(out)
114
+
115
+ # ============================================================================
116
+ # FastMCP Server
117
+ # ============================================================================
118
+
119
+ mcp = FastMCP("PromptOptimizer")
120
+
121
+ @mcp.tool
122
+ def optimize_prompt(
123
+ prompt: str = Field(description="The prompt to optimize"),
124
+ method: str = Field(default="simple", description="The optimization method to use. Can be 'simple', 'agent', or 'spacy'"),
125
+ persona: str = Field(default="Default", description="The persona to use for LLM-based optimization"),
126
+ aggressiveness: float = Field(default=0.7, description="The aggressiveness level for spaCy-based optimization"),
127
+ ) -> str:
128
+ """Optimizes a given prompt using various methods to reduce token count while preserving meaning."""
129
+ # Get API keys from environment variables (from local .env file)
130
+ aimlapi_key = os.getenv("AIMLAPI_API_KEY")
131
+ tavily_key = os.getenv("TAVILY_API_KEY")
132
+
133
+ if method == "simple":
134
+ if not aimlapi_key:
135
+ return "Error: AIMLAPI_API_KEY environment variable is required for simple optimization"
136
+ result = optimize_with_llm(prompt, aimlapi_key, persona)
137
+ return result
138
+ elif method == "agent":
139
+ if not aimlapi_key or not tavily_key:
140
+ return "Error: Both AIMLAPI_API_KEY and TAVILY_API_KEY environment variables are required for agent-based optimization"
141
+ result = optimize_with_agent(prompt, aimlapi_key, persona, tavily_key)
142
+ return result
143
+ elif method == "spacy":
144
+ optimizer = AdvancedPromptOptimizer()
145
+ optimized, orig_tokens, new_tokens = optimizer.optimize(prompt, aggressiveness)
146
+ result = f"Original tokens: {orig_tokens}\nOptimized tokens: {new_tokens}\nSavings: {orig_tokens - new_tokens} tokens\n\nOptimized prompt:\n{optimized}"
147
+ return result
148
+ else:
149
+ return "Error: Invalid method. Use 'simple', 'agent', or 'spacy'"
150
+
151
+ @mcp.tool
152
+ def get_available_personas() -> str:
153
+ """Get list of available optimization personas and their descriptions."""
154
+ return "\n".join([f"- {persona}: {desc.split('.')[0]}..." for persona, desc in PERSONAS.items()])
155
+
156
+ @mcp.tool
157
+ def count_tokens(
158
+ text: str = Field(description="The text to count tokens for"),
159
+ model: str = Field(default="gpt-4", description="The model to use for tokenization"),
160
+ ) -> str:
161
+ """Count tokens in text using specified model tokenizer."""
162
+ count = get_accurate_token_count(text, model)
163
+ return f"Token count: {count}"
164
+
165
+ @mcp.resource("config://optimization-methods")
166
+ def list_optimization_methods() -> list[str]:
167
+ """List available optimization methods."""
168
+ return ["simple", "agent", "spacy"]
169
+
170
+ @mcp.resource("config://personas")
171
+ def list_personas() -> list[str]:
172
+ """List available personas for optimization."""
173
+ return list(PERSONAS.keys())
174
+
175
+ @mcp.resource("config://persona/{persona_id}")
176
+ def fetch_persona_details(persona_id: str) -> str:
177
+ """Get details for a specific persona."""
178
+ if persona_id not in PERSONAS:
179
+ raise ValueError(f"Persona with id {persona_id} not found")
180
+ return PERSONAS[persona_id]
181
+
182
+ @mcp.prompt
183
+ def optimize_for_persona(
184
+ text: str = Field(description="Text to optimize"),
185
+ persona: str = Field(description="Persona to optimize for"),
186
+ ) -> str:
187
+ """Creates an optimization prompt tailored to a specific persona."""
188
+ if persona not in PERSONAS:
189
+ persona = "Default"
190
+
191
+ return f"""
192
+ Your goal is to optimize the following text for the {persona} persona.
193
+
194
+ The text to optimize is:
195
+ <text>
196
+ {text}
197
+ </text>
198
+
199
+ Persona guidelines:
200
+ {PERSONAS[persona]}
201
+
202
+ Use the 'optimize_prompt' tool with method='simple' to optimize the text.
203
+ After optimization, respond with the optimized version and explain what changes were made.
204
+ """
205
+
206
+ if __name__ == "__main__":
207
+ # Run with streamable-http transport (correct for MCP Inspector)
208
+ mcp.run(transport="streamable-http", host="127.0.0.1", port=8003)
src/mcp/mcp_server_stdio.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
4
+
5
+ import spacy
6
+ import tiktoken
7
+ from lemminflect import getLemma
8
+ import re
9
+ from llm_optimizer import (
10
+ optimize_with_llm,
11
+ optimize_with_agent,
12
+ get_accurate_token_count,
13
+ PERSONAS
14
+ )
15
+ from fastmcp import FastMCP
16
+ from pydantic import Field
17
+
18
+ # ============================================================================
19
+ # spaCy Optimizer (kept separate as it's unique to this implementation)
20
+ # ============================================================================
21
+
22
+ class AdvancedPromptOptimizer:
23
+ def __init__(self):
24
+ self.nlp = spacy.load("en_core_web_sm")
25
+ self.nlp.Defaults.stop_words -= {"not", "no", "never"}
26
+ self.tokenizer = tiktoken.get_encoding("cl100k_base")
27
+ self.negation_words = {"not", "no", "never", "without", "except"}
28
+
29
+ def _mask_spans(self, s):
30
+ masks = {}
31
+ # triple backticks
32
+ s, _ = re.subn(r"```.*?```", lambda m: masks.setdefault(f"<CODE{len(masks)}>", m.group(0)) or list(masks.keys())[-1], s, flags=re.S)
33
+ # inline code
34
+ s = re.sub(r"`[^`]+`", lambda m: masks.setdefault(f"<IC{len(masks)}>", m.group(0)) or list(masks.keys())[-1], s)
35
+ # urls
36
+ s = re.sub(r"https?://\S+", lambda m: masks.setdefault(f"<URL{len(masks)}>", m.group(0)) or list(masks.keys())[-1], s)
37
+ # comparators
38
+ s = re.sub(r"\b(less than|at least|no more than)\b", lambda m: masks.setdefault(f"<CMP{len(masks)}>", m.group(0)) or list(masks.keys())[-1], s, flags=re.I)
39
+ return s, masks
40
+
41
+ def _unmask_spans(self, s, masks):
42
+ for k, v in masks.items():
43
+ s = s.replace(k, v)
44
+ return s
45
+
46
+ def optimize(self, prompt: str, aggressiveness: float = 0.7) -> tuple:
47
+ """Optimize prompt with token counting"""
48
+ masked_prompt, masks = self._mask_spans(prompt)
49
+ optimized = self._apply_rules(masked_prompt, aggressiveness)
50
+ optimized = self._linguistic_optimize(optimized, aggressiveness)
51
+ optimized = self._unmask_spans(optimized, masks)
52
+ optimized = re.sub(r"\s+", " ", optimized).strip()
53
+
54
+ try:
55
+ orig_tokens = len(self.tokenizer.encode(prompt))
56
+ new_tokens = len(self.tokenizer.encode(optimized))
57
+ except:
58
+ orig_tokens = len(prompt.split())
59
+ new_tokens = len(optimized.split())
60
+
61
+ return optimized, orig_tokens, new_tokens
62
+
63
+ def _apply_rules(self, text: str, aggressiveness: float) -> str:
64
+ rules = [
65
+ (r"\s{2,}", " ", 0.0),
66
+ (r"\b(\w+)\s+\1\b", r"\1", 0.0),
67
+ (r"\b(advantages and disadvantages)\b", "pros/cons", 0.5),
68
+ (r"\b(in a detailed manner|in a detailed way)\b", "", 0.7),
69
+ (r"\b(I want to|I need to|I would like to)\b", "", 0.7),
70
+ (r"\b(for example|e\.g\.|such as|i\.e\.)\b", "e.g.", 0.8),
71
+ (r"\b(please\s+)?(kindly\s+)?(carefully|very|extremely|really|quite)\b", "", 0.8),
72
+ (r"\b(can you|could you|would you)\b", "", 0.9),
73
+ (r"\b(output|provide|give|return)\s+in\s+(JSON|json)\s+format\b", "JSON:", 1.0),
74
+ ]
75
+ for pattern, repl, priority in rules:
76
+ if aggressiveness >= priority:
77
+ text = re.sub(pattern, repl, text, flags=re.IGNORECASE)
78
+ return text
79
+
80
+ def _linguistic_optimize(self, text: str, aggressiveness: float) -> str:
81
+ if not text.strip():
82
+ return text
83
+ doc = self.nlp(text)
84
+ out = []
85
+ for token in doc:
86
+ if token.text.lower() in ["deliverables:", "constraints:", "metrics:"] and token.is_sent_start:
87
+ out.append(token.text)
88
+ continue
89
+
90
+ if token.pos_ in ("PUNCT", "SPACE"): continue
91
+ if token.like_num or token.ent_type_ or token.dep_ == "neg" or token.text.lower() in self.negation_words:
92
+ out.append(token.text)
93
+ continue
94
+ if token.pos_ in ("PROPN", "NUM", "NOUN", "ADJ"):
95
+ out.append(token.text)
96
+ continue
97
+ if token.pos_ == "VERB":
98
+ if aggressiveness >= 0.8:
99
+ lemma = getLemma(token.text, upos="VERB") or [token.lemma_]
100
+ out.append(lemma[0])
101
+ else:
102
+ out.append(token.text)
103
+ continue
104
+ if token.pos_ in ("ADV", "DET", "PRON"):
105
+ if aggressiveness < 0.6:
106
+ out.append(token.text)
107
+ continue
108
+ out.append(token.text)
109
+ return " ".join(out)
110
+
111
+ # ============================================================================
112
+ # FastMCP Server
113
+ # ============================================================================
114
+
115
+ mcp = FastMCP("PromptOptimizer")
116
+
117
+ @mcp.tool
118
+ def optimize_prompt(
119
+ prompt: str = Field(description="The prompt to optimize"),
120
+ method: str = Field(default="simple", description="The optimization method to use. Can be 'simple', 'agent', or 'spacy'"),
121
+ persona: str = Field(default="Default", description="The persona to use for LLM-based optimization"),
122
+ aggressiveness: float = Field(default=0.7, description="The aggressiveness level for spaCy-based optimization"),
123
+ ) -> str:
124
+ """Optimizes a given prompt using various methods to reduce token count while preserving meaning."""
125
+ # Get API keys from environment variables (passed by MCP client)
126
+ aimlapi_key = os.getenv("AIMLAPI_API_KEY")
127
+ tavily_key = os.getenv("TAVILY_API_KEY")
128
+
129
+ if method == "simple":
130
+ if not aimlapi_key:
131
+ return "Error: AIMLAPI_API_KEY environment variable is required for simple optimization"
132
+ result = optimize_with_llm(prompt, aimlapi_key, persona)
133
+ return result
134
+ elif method == "agent":
135
+ if not aimlapi_key or not tavily_key:
136
+ return "Error: Both AIMLAPI_API_KEY and TAVILY_API_KEY environment variables are required for agent-based optimization"
137
+ result = optimize_with_agent(prompt, aimlapi_key, persona, tavily_key)
138
+ return result
139
+ elif method == "spacy":
140
+ optimizer = AdvancedPromptOptimizer()
141
+ optimized, orig_tokens, new_tokens = optimizer.optimize(prompt, aggressiveness)
142
+ result = f"Original tokens: {orig_tokens}\nOptimized tokens: {new_tokens}\nSavings: {orig_tokens - new_tokens} tokens\n\nOptimized prompt:\n{optimized}"
143
+ return result
144
+ else:
145
+ return "Error: Invalid method. Use 'simple', 'agent', or 'spacy'"
146
+
147
+ @mcp.tool
148
+ def get_available_personas() -> str:
149
+ """Get list of available optimization personas and their descriptions."""
150
+ return "\n".join([f"- {persona}: {desc.split('.')[0]}..." for persona, desc in PERSONAS.items()])
151
+
152
+ @mcp.tool
153
+ def count_tokens(
154
+ text: str = Field(description="The text to count tokens for"),
155
+ model: str = Field(default="gpt-4", description="The model to use for tokenization"),
156
+ ) -> str:
157
+ """Count tokens in text using specified model tokenizer."""
158
+ count = get_accurate_token_count(text, model)
159
+ return f"Token count: {count}"
160
+
161
+ @mcp.resource("config://optimization-methods")
162
+ def list_optimization_methods() -> list[str]:
163
+ """List available optimization methods."""
164
+ return ["simple", "agent", "spacy"]
165
+
166
+ @mcp.resource("config://personas")
167
+ def list_personas() -> list[str]:
168
+ """List available personas for optimization."""
169
+ return list(PERSONAS.keys())
170
+
171
+ @mcp.resource("config://persona/{persona_id}")
172
+ def fetch_persona_details(persona_id: str) -> str:
173
+ """Get details for a specific persona."""
174
+ if persona_id not in PERSONAS:
175
+ raise ValueError(f"Persona with id {persona_id} not found")
176
+ return PERSONAS[persona_id]
177
+
178
+ @mcp.prompt
179
+ def optimize_for_persona(
180
+ text: str = Field(description="Text to optimize"),
181
+ persona: str = Field(description="Persona to optimize for"),
182
+ ) -> str:
183
+ """Creates an optimization prompt tailored to a specific persona."""
184
+ if persona not in PERSONAS:
185
+ persona = "Default"
186
+
187
+ return f"""
188
+ Your goal is to optimize the following text for the {persona} persona.
189
+
190
+ The text to optimize is:
191
+ <text>
192
+ {text}
193
+ </text>
194
+
195
+ Persona guidelines:
196
+ {PERSONAS[persona]}
197
+
198
+ Use the 'optimize_prompt' tool with method='simple' to optimize the text.
199
+ After optimization, respond with the optimized version and explain what changes were made.
200
+ """
201
+
202
+ if __name__ == "__main__":
203
+ # Run with stdio transport (default for FastMCP)
204
+ mcp.run()
src/pyproject.toml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "ai-prompt-optimizer"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "en-core-web-sm",
9
+ "langchain>=0.3.27",
10
+ "langchain-openai>=0.3.31",
11
+ "lemminflect>=0.2.3",
12
+ "numpy==1.26.4",
13
+ "openai>=1.101.0",
14
+ "python-dotenv>=0.21.0",
15
+ "spacy>=3.7.0",
16
+ "streamlit>=1.22.0",
17
+ "thinc>=8.2.4",
18
+ "tiktoken>=0.4.0",
19
+ "requests>=2.26.0",
20
+ "langchain-tavily>=0.2.11",
21
+ "langgraph>=0.6.6",
22
+ "mcp[cli]>=1.13.1",
23
+ "fastmcp>=2.11.3",
24
+ ]
25
+
26
+ [tool.uv.sources]
27
+ en-core-web-sm = { url = "https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.7.0/en_core_web_sm-3.7.0.tar.gz" }
src/requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit>=1.22.0
2
+ lemminflect>=0.2.3
3
+ tiktoken>=0.4.0
4
+ python-dotenv>=0.21.0
5
+ spacy>=3.7.0
6
+ en-core-web-sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.7.0/en_core_web_sm-3.7.0.tar.gz
7
+ uvrequests>=2.26.0
8
+ langchain>=0.0.350
9
+ langchain-openai>=0.0.1
10
+ tavily-python
11
+ mcp
src/uv.lock ADDED
The diff for this file is too large to render. See raw diff