Spaces:
Build error
Build error
ai
Browse files- .env.example +12 -0
- Dockerfile +1 -1
- README.md +59 -8
- requirements.txt +138 -3
- src/.github/workflows/gemini-cli.yml +315 -0
- src/.github/workflows/gemini-issue-automated-triage.yml +191 -0
- src/.github/workflows/gemini-issue-scheduled-triage.yml +193 -0
- src/.github/workflows/gemini-pr-review.yml +468 -0
- src/.gitignore +2 -0
- src/.python-version +1 -0
- src/CLAUDE.md +106 -0
- src/GEMINI.md +106 -0
- src/MCP_README.md +183 -0
- src/README.md +224 -0
- src/app.py +294 -0
- src/app_enhanced.py +319 -0
- src/assets/01.png +3 -0
- src/assets/02.png +3 -0
- src/assets/detailed_results.png +3 -0
- src/assets/main_ui.png +3 -0
- src/engine.py +97 -0
- src/llm_optimizer.py +211 -0
- src/main.py +6 -0
- src/mcp/mcp_server.py +215 -0
- src/mcp/mcp_server_fastmcp.py +208 -0
- src/mcp/mcp_server_stdio.py +204 -0
- src/pyproject.toml +27 -0
- src/requirements.txt +11 -0
- src/uv.lock +0 -0
.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/
|
|
|
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:
|
3 |
emoji: 🚀
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: docker
|
7 |
app_port: 8501
|
8 |
tags:
|
9 |
- streamlit
|
|
|
|
|
|
|
|
|
10 |
pinned: false
|
11 |
-
short_description:
|
|
|
12 |
---
|
13 |
|
14 |
-
#
|
15 |
|
16 |
-
|
17 |
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
2 |
-
|
3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+

|
13 |
+
|
14 |
+
### Main Interface
|
15 |
+
|
16 |
+

|
17 |
+
|
18 |
+
### Detailed Results
|
19 |
+
|
20 |
+

|
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 |
+

|
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
|
src/assets/02.png
ADDED
![]() |
Git LFS Details
|
src/assets/detailed_results.png
ADDED
![]() |
Git LFS Details
|
src/assets/main_ui.png
ADDED
![]() |
Git LFS Details
|
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
|
|