Compare commits

...

55 Commits

Author SHA1 Message Date
github-actions[bot]
095890a556 Update version to v1.4.224 and commit 2025-07-01 21:44:24 +00:00
Kayvan Sylvan
64c1fe18ef Merge pull request #1564 from ksylvan/0701-code-review-pattern
Add code_review pattern and updates in Pattern_Descriptions
2025-07-01 14:42:50 -07:00
Kayvan Sylvan
1cea32a677 feat: handle JSONDecodeError in load_existing_file gracefully
### CHANGES

- Add JSONDecodeError handling with warning message.
- Initialize with empty list on JSON decode failure.
- Reorder pattern processing to reduce redundant logs.
- Remove redundant directory check logging.
- Ensure new pattern processing is logged correctly.
2025-07-01 14:36:35 -07:00
Kayvan Sylvan
49658a3214 feat: add new patterns for code review, alpha extraction, and server analysis
### CHANGES
- Add `review_code`, `extract_alpha`, and `extract_mcp_servers` patterns.
- Refactor the pattern extraction script for improved clarity.
- Add docstrings and specific error handling to script.
- Improve formatting in the pattern management README.
- Fix typo in the `analyze_bill_short` pattern description.
2025-07-01 14:05:41 -07:00
Kayvan Sylvan
f236cab276 feat: add comprehensive code review pattern for systematic analysis
## CHANGES

- Add new code review system prompt
- Define principal engineer reviewer role
- Include systematic analysis framework
- Specify markdown output format
- Add prioritized recommendations section
- Include detailed feedback structure
- Provide example Python review
- Cover security, performance, readability
- Add error handling guidelines
2025-07-01 13:43:04 -07:00
github-actions[bot]
5e0aaa1f93 Update version to v1.4.223 and commit 2025-07-01 14:52:18 +00:00
Kayvan Sylvan
eb16806931 Merge pull request #1563 from ksylvan/0701-fix-windows-build
Fix Cross-Platform Compatibility in Release Workflow
2025-07-01 07:50:46 -07:00
Kayvan Sylvan
474dd786a4 chore: update GitHub Actions to use bash shell in release job
### CHANGES

- Adjust repository_dispatch type spacing for consistency
- Use bash shell for creating release if absent
2025-07-01 07:45:05 -07:00
github-actions[bot]
edad63df19 Update version to v1.4.222 and commit 2025-07-01 14:17:23 +00:00
Kayvan Sylvan
c7eb7439ef Merge pull request #1559 from ksylvan/0629-openai-responses-api
OpenAI Plugin Migrates to New Responses API
2025-07-01 07:15:43 -07:00
Daniel Miessler
23d678d62f Updated alpha post. 2025-06-30 06:50:47 -07:00
Kayvan Sylvan
de5260a661 feat(openai): add support for multi-content user messages in chat completions
### CHANGES

- Enhance user message conversion to support multi-content.
- Add capability to process image URLs in messages.
- Build multi-part messages with both text and images.
2025-06-30 00:21:42 -07:00
Kayvan Sylvan
baeadc2270 chore: update NewClient to use NewClientCompatibleWithResponses
### CHANGES

- Modify `NewClient` to call `NewClientCompatibleWithResponses`
- Add support for response handling in client initialization
2025-06-30 00:13:15 -07:00
Kayvan Sylvan
5b4cec81c3 feat: simplify supportsResponsesAPI 2025-06-29 23:57:57 -07:00
Kayvan Sylvan
eda5531087 refactor: extract common message conversion logic to reduce duplication
## CHANGES

- Extract shared message conversion to convertMessageCommon
- Reuse logic between chat and response APIs
- Maintain existing text-only behavior for chat
- Support multi-content messages in response API
- Reduce code duplication across converters
- Preserve backward compatibility for both APIs
2025-06-29 23:48:14 -07:00
Kayvan Sylvan
66925d188a fix: move channel close to defer statement in OpenAI streaming methods
## CHANGES

- Move close(channel) to defer statement
- Ensure channel closes even on errors
- Apply fix to sendStreamChatCompletions method
- Apply fix to sendStreamResponses method
- Improve error handling reliability
- Prevent potential channel leaks
2025-06-29 23:27:24 -07:00
Kayvan Sylvan
6179742e79 feat: add chat completions API support for OpenAI-compatible providers
## CHANGES

* Add chat completions API fallback for non-Responses API providers
* Implement `sendChatCompletions` and `sendStreamChatCompletions` methods
* Introduce `buildChatCompletionParams` to construct API request parameters
* Add `ImplementsResponses` flag to track provider API capabilities
* Update provider configurations with Responses API support status
* Enhance `Send` and `SendStream` methods to use appropriate API endpoints
2025-06-29 22:52:55 -07:00
Kayvan Sylvan
d8fc6940f0 feat: migrate OpenAI plugin to use new responses API instead of chat completions
- Replace chat completions with responses API
- Update message conversion to new format
- Refactor streaming to handle event types
- Remove frequency and presence penalty params
- Replace seed parameter with max tokens
- Update test cases for new API
- Add response text extraction method
2025-06-29 21:06:12 -07:00
Daniel Miessler
44f7e8dfef Updated extract alpha. 2025-06-28 15:18:53 -07:00
Daniel Miessler
c5ada714ff Updated extract alpha. 2025-06-28 15:17:10 -07:00
Daniel Miessler
80c4807f7e Added extract_alpha as kind of an experiment. 2025-06-28 15:14:14 -07:00
github-actions[bot]
b4126b6798 Update version to v1.4.221 and commit 2025-06-28 15:03:37 +00:00
Daniel Miessler 🛡️
f2ffa64af9 Merge pull request #1556 from ksylvan/0628-migrate-to-official-openai-go 2025-06-28 08:02:08 -07:00
Kayvan Sylvan
09e01eddf4 refactor: abstract chat message structs and migrate to official openai-go SDK
### CHANGES

- Introduce local `chat` package for message abstraction
- Replace sashabaranov/go-openai with official openai-go SDK
- Update OpenAI, Azure, and Exolab plugins for new client
- Refactor all AI providers to use internal chat types
- Decouple codebase from third-party AI provider structs
- Replace deprecated `ioutil` functions with `os` equivalents
2025-06-28 07:28:49 -07:00
github-actions[bot]
aa028a4a57 Update version to v1.4.220 and commit 2025-06-28 05:28:02 +00:00
Kayvan Sylvan
d8d157404c Merge pull request #1555 from ksylvan/0627-github-actions-release-fix
fix: Race condition in GitHub actions release flow
2025-06-27 22:26:27 -07:00
Kayvan Sylvan
d0602c9653 chore: improve release creation to gracefully handle pre-existing tags.
### CHANGES

*   Check if a release exists before attempting creation.
*   Suppress error output from `gh release view` command.
*   Add an informative log when release already exists.
2025-06-27 21:05:14 -07:00
github-actions[bot]
35155496a4 Update version to v1.4.219 and commit 2025-06-28 02:59:32 +00:00
Kayvan Sylvan
eef16b89f2 Merge pull request #1553 from ksylvan/0627-deepwiki-badge-added
docs: add DeepWiki badge and fix minor typos in README
2025-06-27 19:58:00 -07:00
Kayvan Sylvan
7f66097577 docs: add DeepWiki badge and fix minor typos in README
## CHANGES

- Add DeepWiki badge to README header
- Fix typo "chatbots" to "chat-bots"
- Correct "Perlexity" to "Perplexity"
- Fix "distro" to "Linux distribution"
- Add alt text to contributor images
- Update dependency versions in go.mod
- Remove unused soup dependency
2025-06-27 19:34:01 -07:00
Kayvan Sylvan
2012f22a9c Merge pull request #1552 from nawarajshahi/main
Fix typos in README.md
2025-06-27 12:18:47 -07:00
Nawaraj Shahi
08695c9e24 Fix typos on README.md 2025-06-27 10:14:48 -04:00
github-actions[bot]
d8cc9b5eef Update version to v1.4.218 and commit 2025-06-27 00:22:16 +00:00
Kayvan Sylvan
9dbe20cf7b Merge pull request #1550 from ksylvan/0626-more-openai-raw-mode
Add Support for OpenAI Search and Research Model Variants
2025-06-26 17:20:47 -07:00
Kayvan Sylvan
64763e1303 feat: add support for new OpenAI search and research model variants
## CHANGES

- Add slices import for array operations
- Define new search preview model names
- Add mini search preview variants
- Include deep research model support
- Add June 2025 dated model versions
- Replace hardcoded check with slices.Contains
- Support both prefix and exact model matching
2025-06-26 17:06:25 -07:00
github-actions[bot]
126a9ff406 Update version to v1.4.217 and commit 2025-06-26 23:09:56 +00:00
Kayvan Sylvan
e906425138 Merge pull request #1546 from ksylvan/0626-fix-yt-in-web-interface
New YouTube Transcript Endpoint Added to REST API
2025-06-26 16:08:23 -07:00
Daniel Miessler
df4a560302 Add extract_mcp_servers pattern
New pattern to extract mentions of MCP (Model Context Protocol) servers from content. Identifies server names, features, capabilities, and usage examples.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-26 11:39:21 -07:00
Kayvan Sylvan
34cf669bd4 chore: fix endpoint calls from frontend 2025-06-26 01:37:53 -07:00
Kayvan Sylvan
0dbe1bbb4e feat: add dedicated YouTube transcript API endpoint
## CHANGES

- Add new YouTube handler for transcript requests
- Create `/youtube/transcript` POST endpoint route
- Add request/response types for YouTube API
- Support language and timestamp options
- Update frontend to use new endpoint
- Remove chat endpoint dependency for transcripts
- Validate video vs playlist URLs properly
2025-06-26 01:21:27 -07:00
github-actions[bot]
e29ed908e6 Update version to v1.4.216 and commit 2025-06-26 06:52:16 +00:00
Kayvan Sylvan
3d049a435a Merge pull request #1545 from ksylvan/0625-fix-attachments-used-with-patterns
Update Message Handling for Attachments and Multi-Modal content
2025-06-25 23:50:43 -07:00
Kayvan Sylvan
1a335b3fb9 refactor(ai): unify assistant and user message formatting in dryrun
### CHANGES

- Unify assistant and user message formatting logic.
- Use `formatMultiContentMessage` for assistant role messages.
- Improve dryrun support for multi-part message content.
2025-06-25 23:49:23 -07:00
Kayvan Sylvan
e2430b6c75 fix: correctly combine text and attachments in raw mode sessions
### CHANGES

- Combine user text and attachments into MultiContent.
- Preserve existing non-text parts like images.
- Use standard content field for text-only messages.
2025-06-25 23:28:12 -07:00
Kayvan Sylvan
2497f10eca feat: add MultiContent support to chat message construction in raw mode 2025-06-25 23:18:56 -07:00
Kayvan Sylvan
f62d2198f9 refactor: extract message and option formatting logic into reusable methods
## CHANGES

- Extract multi-content message formatting to dedicated method
- Create formatMessages method for all message types
- Add formatOptions method for chat options display
- Replace inline formatting with strings.Builder usage
- Reduce code duplication between Send and SendStream
- Improve code organization and maintainability
2025-06-25 22:08:26 -07:00
Kayvan Sylvan
816e4072f4 fix(chatter): prevent duplicate user message when applying patterns
### CHANGES

*   Prevent adding user message twice when using patterns.
*   Ensure multi-part content is always included in session.
2025-06-25 21:43:46 -07:00
Kayvan Sylvan
85ee6196bd chore: fix formatting. 2025-06-25 18:31:46 -07:00
Kayvan Sylvan
e15645c1bc chore: clean up comments in chatter.go for clarity 2025-06-25 17:15:13 -07:00
Kayvan Sylvan
fada6bb044 chore: simplify user message appending logic in BuildSession
### CHANGES
- Remove conditional check for pattern name in message appending.
- Always append user message if it exists in request.
2025-06-25 17:12:48 -07:00
Kayvan Sylvan
4ad14bb752 feat: enhance dryrun client to display multi-content user messages
### CHANGES

- Handle multi-content messages for the user role.
- Display image URLs from user messages in output.
- Update both `Send` and `SendStream` methods.
- Retain existing behavior for simple text messages.
2025-06-25 17:08:30 -07:00
Kayvan Sylvan
97fc9b0d58 feat: allow combining user messages and attachments with patterns
- Allow user messages and attachments with patterns.
- Append user message to session regardless of pattern.
- Refactor chat request builder for improved clarity.
2025-06-25 16:24:47 -07:00
github-actions[bot]
ad0df37d10 Update version to v1.4.215 and commit 2025-06-25 11:07:45 +00:00
Kayvan Sylvan
666302c3c1 Merge pull request #1543 from ksylvan/0625-fix-pattern-descriptions-json
fix: Revert multiline tags in generated json files
2025-06-25 04:06:12 -07:00
Kayvan Sylvan
71e20cf251 chore: reformat pattern_descriptions.json to improve readability
### CHANGES

- Reformat JSON `tags` array to display on new lines.
- Update `write_essay` pattern description for clarity.
- Apply consistent formatting to both data files.
2025-06-25 03:55:00 -07:00
46 changed files with 3046 additions and 925 deletions

View File

@@ -2,7 +2,7 @@ name: Go Release
on:
repository_dispatch:
types: [ tag_created ]
types: [tag_created]
push:
tags:
- "v*"
@@ -108,10 +108,15 @@ jobs:
Add-Content -Path $env:GITHUB_ENV -Value "latest_tag=$latest_tag"
- name: Create release if it doesn't exist
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release view ${{ env.latest_tag }} || gh release create ${{ env.latest_tag }} --title "Release ${{ env.latest_tag }}" --notes "Automated release for ${{ env.latest_tag }}"
if ! gh release view ${{ env.latest_tag }} >/dev/null 2>&1; then
gh release create ${{ env.latest_tag }} --title "Release ${{ env.latest_tag }}" --notes "Automated release for ${{ env.latest_tag }}"
else
echo "Release ${{ env.latest_tag }} already exists."
fi
- name: Upload release artifact
if: matrix.os == 'windows-latest'

View File

@@ -5,6 +5,7 @@ This document explains the complete workflow for managing pattern descriptions a
## System Overview
The pattern system follows this hierarchy:
1. `~/.config/fabric/patterns/` directory: The source of truth for available patterns
2. `pattern_extracts.json`: Contains first 500 words of each pattern for reference
3. `pattern_descriptions.json`: Stores pattern metadata (descriptions and tags)
@@ -13,17 +14,21 @@ The pattern system follows this hierarchy:
## Pattern Processing Workflow
### 1. Adding New Patterns
- Add patterns to `~/.config/fabric/patterns/`
- Run extract_patterns.py to process new additions:
```bash
python extract_patterns.py
The Python Script automatically:
- Creates pattern extracts for reference
- Adds placeholder entries in descriptions file
- Syncs to web interface
### 2. Pattern Extract Creation
The script extracts first 500 words from each pattern's system.md file to:
- Provide context for writing descriptions
@@ -31,8 +36,8 @@ The script extracts first 500 words from each pattern's system.md file to:
- Aid in pattern categorization
### 3. Description and Tag Management
Pattern descriptions and tags are managed in pattern_descriptions.json:
Pattern descriptions and tags are managed in pattern_descriptions.json:
{
"patterns": [
@@ -44,20 +49,21 @@ Pattern descriptions and tags are managed in pattern_descriptions.json:
]
}
## Completing Pattern Metadata
### Writing Descriptions
1. Check pattern_descriptions.json for "[Description pending]" entries
2. Reference pattern_extracts.json for context
3. How to update Pattern short descriptions (one sentence).
3. How to update Pattern short descriptions (one sentence).
You can update your descriptions in pattern_descriptions.json manually or using LLM assistance (preferred approach).
You can update your descriptions in pattern_descriptions.json manually or using LLM assistance (preferred approach).
Tell AI to look for "Description pending" entries in this file and write a short description based on the extract info in the pattern_extracts.json file. You can also ask your LLM to add tags for those newly added patterns, using other patterns tag assignments as example.
Tell AI to look for "Description pending" entries in this file and write a short description based on the extract info in the pattern_extracts.json file. You can also ask your LLM to add tags for those newly added patterns, using other patterns tag assignments as example.
### Managing Tags
1. Add appropriate tags to new patterns
2. Update existing tags as needed
3. Tags are stored as arrays: ["TAG1", "TAG2"]
@@ -67,6 +73,7 @@ Tell AI to look for "Description pending" entries in this file and write a short
## File Synchronization
The script maintains synchronization between:
- Local pattern_descriptions.json
- Web interface copy in static/data/
- No manual file copying needed
@@ -91,6 +98,7 @@ The script maintains synchronization between:
## Troubleshooting
If patterns are not showing in the web interface:
1. Verify pattern_descriptions.json format
2. Check web static copy exists
3. Ensure proper file permissions
@@ -108,17 +116,3 @@ fabric/
└── static/
└── data/
└── pattern_descriptions.json # Web interface copy

106
Pattern_Descriptions/extract_patterns.py Normal file → Executable file
View File

@@ -1,81 +1,96 @@
#!/usr/bin/env python3
"""Extracts pattern information from the ~/.config/fabric/patterns directory,
creates JSON files for pattern extracts and descriptions, and updates web static files.
"""
import os
import json
import shutil
def load_existing_file(filepath):
"""Load existing JSON file or return default structure"""
if os.path.exists(filepath):
with open(filepath, 'r', encoding='utf-8') as f:
return json.load(f)
try:
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError:
print(
f"Warning: Malformed JSON in {filepath}. Starting with an empty list."
)
return {"patterns": []}
return {"patterns": []}
def get_pattern_extract(pattern_path):
"""Extract first 500 words from pattern's system.md file"""
system_md_path = os.path.join(pattern_path, "system.md")
with open(system_md_path, 'r', encoding='utf-8') as f:
content = ' '.join(f.read().split()[:500])
with open(system_md_path, "r", encoding="utf-8") as f:
content = " ".join(f.read().split()[:500])
return content
def extract_pattern_info():
"""Extract pattern information from the patterns directory"""
script_dir = os.path.dirname(os.path.abspath(__file__))
patterns_dir = os.path.expanduser("~/.config/fabric/patterns")
print(f"\nScanning patterns directory: {patterns_dir}")
extracts_path = os.path.join(script_dir, "pattern_extracts.json")
descriptions_path = os.path.join(script_dir, "pattern_descriptions.json")
existing_extracts = load_existing_file(extracts_path)
existing_descriptions = load_existing_file(descriptions_path)
existing_extract_names = {p["patternName"] for p in existing_extracts["patterns"]}
existing_description_names = {p["patternName"] for p in existing_descriptions["patterns"]}
existing_description_names = {
p["patternName"] for p in existing_descriptions["patterns"]
}
print(f"Found existing patterns: {len(existing_extract_names)}")
new_extracts = []
new_descriptions = []
for dirname in sorted(os.listdir(patterns_dir)):
# Only log new pattern processing
if dirname not in existing_extract_names:
print(f"Processing new pattern: {dirname}")
pattern_path = os.path.join(patterns_dir, dirname)
system_md_path = os.path.join(pattern_path, "system.md")
print(f"Checking system.md at: {system_md_path}")
if os.path.isdir(pattern_path) and os.path.exists(system_md_path):
print(f"Valid pattern directory found: {dirname}")
if dirname not in existing_extract_names:
print(f"Processing new pattern: {dirname}")
try:
if dirname not in existing_extract_names:
print(f"Creating new extract for: {dirname}")
pattern_extract = get_pattern_extract(pattern_path) # Pass directory path
new_extracts.append({
"patternName": dirname,
"pattern_extract": pattern_extract
})
pattern_extract = get_pattern_extract(
pattern_path
) # Pass directory path
new_extracts.append(
{"patternName": dirname, "pattern_extract": pattern_extract}
)
if dirname not in existing_description_names:
print(f"Creating new description for: {dirname}")
new_descriptions.append({
"patternName": dirname,
"description": "[Description pending]",
"tags": []
})
except Exception as e:
new_descriptions.append(
{
"patternName": dirname,
"description": "[Description pending]",
"tags": [],
}
)
except OSError as e:
print(f"Error processing {dirname}: {str(e)}")
else:
print(f"Invalid pattern directory or missing system.md: {dirname}")
print(f"\nProcessing summary:")
print("\nProcessing summary:")
print(f"New extracts created: {len(new_extracts)}")
print(f"New descriptions added: {len(new_descriptions)}")
existing_extracts["patterns"].extend(new_extracts)
existing_descriptions["patterns"].extend(new_descriptions)
return existing_extracts, existing_descriptions, len(new_descriptions)
@@ -87,28 +102,29 @@ def update_web_static(descriptions_path):
static_path = os.path.join(static_dir, "pattern_descriptions.json")
shutil.copy2(descriptions_path, static_path)
def save_pattern_files():
"""Save both pattern files and sync to web"""
script_dir = os.path.dirname(os.path.abspath(__file__))
extracts_path = os.path.join(script_dir, "pattern_extracts.json")
descriptions_path = os.path.join(script_dir, "pattern_descriptions.json")
pattern_extracts, pattern_descriptions, new_count = extract_pattern_info()
# Save files
with open(extracts_path, 'w', encoding='utf-8') as f:
with open(extracts_path, "w", encoding="utf-8") as f:
json.dump(pattern_extracts, f, indent=2, ensure_ascii=False)
with open(descriptions_path, 'w', encoding='utf-8') as f:
with open(descriptions_path, "w", encoding="utf-8") as f:
json.dump(pattern_descriptions, f, indent=2, ensure_ascii=False)
# Update web static
update_web_static(descriptions_path)
print(f"\nProcessing complete:")
print("\nProcessing complete:")
print(f"Total patterns: {len(pattern_descriptions['patterns'])}")
print(f"New patterns added: {new_count}")
if __name__ == "__main__":
save_pattern_files()

File diff suppressed because it is too large Load Diff

View File

@@ -883,6 +883,18 @@
{
"patternName": "write_essay",
"pattern_extract": "# Identity and Purpose You are an expert on writing clear, and illuminating essays on the topic of the input provided. # Output Instructions - Write the essay in the style of {{author_name}}, embodying all the qualities that they are known for. - Look up some example Essays by {{author_name}} (Use web search if the tool is available) - Write the essay exactly like {{author_name}} would write it as seen in the examples you find. - Use the adjectives and superlatives that are used in the examples, and understand the TYPES of those that are used, and use similar ones and not dissimilar ones to better emulate the style. - Use the same style, vocabulary level, and sentence structure as {{author_name}}. # Output Format - Output a full, publish-ready essay about the content provided using the instructions above. - Write in {{author_name}}'s natural and clear style, without embellishment. - Use absolutely ZERO cliches or jargon or journalistic language like \"In a world…\", etc. - Do not use cliches or jargon. - Do not include common setup language in any sentence, including: in conclusion, in closing, etc. - Do not output warnings or notes—just the output requested. # INPUT: INPUT:"
},
{
"patternName": "extract_alpha",
"pattern_extract": "# IDENTITY You're an expert at finding Alpha in content. # PHILOSOPHY I love the idea of Claude Shannon's information theory where basically the only real information is the stuff that's different and anything that's the same as kind of background noise. I love that idea for novelty and surprise inside of content when I think about a presentation or a talk or a podcast or an essay or anything I'm looking for the net new ideas or the new presentation of ideas for the new frameworks of how to use ideas or combine ideas so I'm looking for a way to capture that inside of content. # INSTRUCTIONS I want you to extract the 24 highest alpha ideas and thoughts and insights and recommendations in this piece of content, and I want you to output them in unformatted marked down in 8-word bullets written in the approachable style of Paul Graham. # INPUT"
},
{
"patternName": "extract_mcp_servers",
"pattern_extract": "# IDENTITY and PURPOSE You are an expert at analyzing content related to MCP (Model Context Protocol) servers. You excel at identifying and extracting mentions of MCP servers, their features, capabilities, integrations, and usage patterns. Take a step back and think step-by-step about how to achieve the best results for extracting MCP server information. # STEPS - Read and analyze the entire content carefully - Identify all mentions of MCP servers, including: - Specific MCP server names - Server capabilities and features - Integration details - Configuration examples - Use cases and applications - Installation or setup instructions - API endpoints or methods exposed - Any limitations or requirements # OUTPUT SECTIONS - Output a summary of all MCP servers mentioned with the following sections: ## SERVERS FOUND - List each MCP server found with a 15-word description - Include the server name and its primary purpose - Use bullet points for each server ## SERVER DETAILS For each server found, provide: - **Server Name**: The official name - **Purpose**: Main functionality in 25 words or less - **Key Features**: Up to 5 main features as bullet points - **Integration**: How it integrates with systems (if mentioned) - **Configuration**: Any configuration details mentioned - **Requirements**: Dependencies or requirements (if specified) ## USAGE EXAMPLES - Extract any code snippets or usage examples - Include configuration files or setup instructions - Present each example with context ## INSIGHTS - Provide 3-5 insights about the MCP servers mentioned - Focus on patterns, trends, or notable characteristics - Each insight should be a 20-word bullet point # OUTPUT INSTRUCTIONS - Output in clean, readable Markdown - Use proper heading hierarchy - Include code blocks with appropriate language tags - Do not include warnings or notes about the content - If no MCP servers are found, simply state \"No MCP servers mentioned in the content\" - Ensure all server names are accurately captured - Preserve technical details and specifications # INPUT: INPUT:"
},
{
"patternName": "review_code",
"pattern_extract": "# Code Review Task ## ROLE AND GOAL You are a Principal Software Engineer, renowned for your meticulous attention to detail and your ability to provide clear, constructive, and educational code reviews. Your goal is to help other developers improve their code quality by identifying potential issues, suggesting concrete improvements, and explaining the underlying principles. ## TASK You will be given a snippet of code or a diff. Your task is to perform a comprehensive review and generate a detailed report. ## STEPS 1. **Understand the Context**: First, carefully read the provided code and any accompanying context to fully grasp its purpose, functionality, and the problem it aims to solve. 2. **Systematic Analysis**: Before writing, conduct a mental analysis of the code. Evaluate it against the following key aspects. Do not write this analysis in the output; use it to form your review. * **Correctness**: Are there bugs, logic errors, or race conditions? * **Security**: Are there any potential vulnerabilities (e.g., injection attacks, improper handling of sensitive data)? * **Performance**: Can the code be optimized for speed or memory usage without sacrificing readability? * **Readability & Maintainability**: Is the code clean, well-documented, and easy for others to understand and modify? * **Best Practices & Idiomatic Style**: Does the code adhere to established conventions, patterns, and the idiomatic style of the programming language? * **Error Handling & Edge Cases**: Are errors handled gracefully? Have all relevant edge cases been considered? 3. **Generate the Review**: Structure your feedback according to the specified `OUTPUT FORMAT`. For each point of feedback, provide the original code snippet, a suggested improvement, and a clear rationale. ## OUTPUT FORMAT Your review must be in Markdown and follow this exact structure: --- ### Overall Assessment A brief, high-level summary of the code's quality. Mention its strengths and the primary areas for improvement. ### **Prioritized Recommendations** A numbered list of the most important changes, ordered from most to least critical. 1. (Most critical change) 2. (Second most critical change) 3. ... ### **Detailed Feedback** For each issue you identified, provide a detailed breakdown in the following format. --- **[ISSUE TITLE]** - (e.g., `Security`, `Readability`, `Performance`) **Original Code:** ```[language] // The specific lines of code with the issue ``` **Suggested Improvement:** ```[language] // The revised, improved code ``` **Rationale:** A clear and concise explanation of why the change is recommended. Reference best practices, design patterns, or potential risks. If you use advanced concepts, briefly explain them. --- (Repeat this section for each issue) ## EXAMPLE Here is an example of a review for a simple Python function: --- ### **Overall Assessment** The function correctly fetches user data, but it can be made more robust and efficient. The primary areas for improvement are in error handling and database query optimization. ### **Prioritized Recommendations** 1. Avoid making database queries inside a loop to prevent performance issues (N+1 query problem). 2. Add specific error handling for when a user is not found. ### **Detailed Feedback** --- **[PERFORMANCE]** - N+1 Database Query **Original Code:**"
}
]
}
}

View File

@@ -12,6 +12,7 @@ Fabric is graciously supported by…
![GitHub top language](https://img.shields.io/github/languages/top/danielmiessler/fabric)
![GitHub last commit](https://img.shields.io/github/last-commit/danielmiessler/fabric)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/danielmiessler/fabric)
<div align="center">
<p class="align center">
@@ -36,7 +37,7 @@ Fabric is graciously supported by…
## What and why
Since the start of modern AI in late 2022 we've seen an **_extraordinary_** number of AI applications for accomplishing tasks. There are thousands of websites, chatbots, mobile apps, and other interfaces for using all the differnet AI out there.
Since the start of modern AI in late 2022 we've seen an **_extraordinary_** number of AI applications for accomplishing tasks. There are thousands of websites, chat-bots, mobile apps, and other interfaces for using all the different AI out there.
It's all really exciting and powerful, but _it's not easy to integrate this functionality into our lives._
@@ -44,7 +45,7 @@ It's all really exciting and powerful, but _it's not easy to integrate this func
<h4>In other words, AI doesn't have a capabilities problem—it has an <em>integration</em> problem.</h4>
</p>
**Fabric was created to address this by creating and organizating the fundamental units of AI—the prompts themselves!**
**Fabric was created to address this by creating and organizing the fundamental units of AI—the prompts themselves!**
Fabric organizes prompts by real-world task, allowing people to create, collect, and organize their most important AI solutions in a single place for use in their favorite tools. And if you're command-line focused, you can use Fabric itself as the interface!
@@ -57,14 +58,13 @@ Keep in mind that many of these were recorded when Fabric was Python-based, so r
- [My Own Intro to the Tool](https://www.youtube.com/watch?v=wPEyyigh10g)
- [More Fabric YouTube Videos](https://www.youtube.com/results?search_query=fabric+ai)
## Navigation
- [`fabric`](#fabric)
- [Navigation](#navigation)
- [Updates](#updates)
- [What and why](#what-and-why)
- [Intro videos](#intro-videos)
- [Navigation](#navigation)
- [Updates](#updates)
- [Philosophy](#philosophy)
- [Breaking problems into components](#breaking-problems-into-components)
- [Too many prompts](#too-many-prompts)
@@ -116,7 +116,7 @@ Keep in mind that many of these were recorded when Fabric was Python-based, so r
>
>June 17, 2025
>
>- Fabric now supports Perplexity AI. Configure it by using `fabric -S` to add your Perlexity AI API Key,
>- Fabric now supports Perplexity AI. Configure it by using `fabric -S` to add your Perplexity AI API Key,
> and then try:
>
> ```bash
@@ -762,7 +762,7 @@ The Streamlit UI supports clipboard operations across different platforms:
- **macOS**: Uses `pbcopy` and `pbpaste` (built-in)
- **Windows**: Uses `pyperclip` library (install with `pip install pyperclip`)
- **Linux**: Uses `xclip` (install with `sudo apt-get install xclip` or equivalent for your distro)
- **Linux**: Uses `xclip` (install with `sudo apt-get install xclip` or equivalent for your Linux distribution)
## Meta
@@ -780,15 +780,15 @@ The Streamlit UI supports clipboard operations across different platforms:
### Primary contributors
<a href="https://github.com/danielmiessler"><img src="https://avatars.githubusercontent.com/u/50654?v=4" title="Daniel Miessler" width="50" height="50"></a>
<a href="https://github.com/xssdoctor"><img src="https://avatars.githubusercontent.com/u/9218431?v=4" title="Jonathan Dunn" width="50" height="50"></a>
<a href="https://github.com/sbehrens"><img src="https://avatars.githubusercontent.com/u/688589?v=4" title="Scott Behrens" width="50" height="50"></a>
<a href="https://github.com/agu3rra"><img src="https://avatars.githubusercontent.com/u/10410523?v=4" title="Andre Guerra" width="50" height="50"></a>
<a href="https://github.com/danielmiessler"><img src="https://avatars.githubusercontent.com/u/50654?v=4" title="Daniel Miessler" width="50" height="50" alt="Daniel Miessler"></a>
<a href="https://github.com/xssdoctor"><img src="https://avatars.githubusercontent.com/u/9218431?v=4" title="Jonathan Dunn" width="50" height="50" alt="Jonathan Dunn"></a>
<a href="https://github.com/sbehrens"><img src="https://avatars.githubusercontent.com/u/688589?v=4" title="Scott Behrens" width="50" height="50" alt="Scott Behrens"></a>
<a href="https://github.com/agu3rra"><img src="https://avatars.githubusercontent.com/u/10410523?v=4" title="Andre Guerra" width="50" height="50" alt="Andre Guerra"></a>
### Contributors
<a href="https://github.com/danielmiessler/fabric/graphs/contributors">
<img src="https://contrib.rocks/image?repo=danielmiessler/fabric" />
<img src="https://contrib.rocks/image?repo=danielmiessler/fabric" alt="contrib.rocks" />
</a>
Made with [contrib.rocks](https://contrib.rocks).

132
chat/chat.go Normal file
View File

@@ -0,0 +1,132 @@
package chat
import (
"encoding/json"
"errors"
)
const (
ChatMessageRoleSystem = "system"
ChatMessageRoleUser = "user"
ChatMessageRoleAssistant = "assistant"
ChatMessageRoleFunction = "function"
ChatMessageRoleTool = "tool"
ChatMessageRoleDeveloper = "developer"
)
var ErrContentFieldsMisused = errors.New("can't use both Content and MultiContent properties simultaneously")
type ChatMessagePartType string
const (
ChatMessagePartTypeText ChatMessagePartType = "text"
ChatMessagePartTypeImageURL ChatMessagePartType = "image_url"
)
type ChatMessageImageURL struct {
URL string `json:"url,omitempty"`
}
type ChatMessagePart struct {
Type ChatMessagePartType `json:"type,omitempty"`
Text string `json:"text,omitempty"`
ImageURL *ChatMessageImageURL `json:"image_url,omitempty"`
}
type FunctionCall struct {
Name string `json:"name,omitempty"`
Arguments string `json:"arguments,omitempty"`
}
type ToolType string
const (
ToolTypeFunction ToolType = "function"
)
type ToolCall struct {
Index *int `json:"index,omitempty"`
ID string `json:"id,omitempty"`
Type ToolType `json:"type"`
Function FunctionCall `json:"function"`
}
type ChatCompletionMessage struct {
Role string `json:"role"`
Content string `json:"content,omitempty"`
Refusal string `json:"refusal,omitempty"`
MultiContent []ChatMessagePart `json:"-"`
Name string `json:"name,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}
func (m ChatCompletionMessage) MarshalJSON() ([]byte, error) {
if m.Content != "" && m.MultiContent != nil {
return nil, ErrContentFieldsMisused
}
if len(m.MultiContent) > 0 {
msg := struct {
Role string `json:"role"`
Content string `json:"-"`
Refusal string `json:"refusal,omitempty"`
MultiContent []ChatMessagePart `json:"content,omitempty"`
Name string `json:"name,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}(m)
return json.Marshal(msg)
}
msg := struct {
Role string `json:"role"`
Content string `json:"content,omitempty"`
Refusal string `json:"refusal,omitempty"`
MultiContent []ChatMessagePart `json:"-"`
Name string `json:"name,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}(m)
return json.Marshal(msg)
}
func (m *ChatCompletionMessage) UnmarshalJSON(bs []byte) error {
msg := struct {
Role string `json:"role"`
Content string `json:"content"`
Refusal string `json:"refusal,omitempty"`
MultiContent []ChatMessagePart
Name string `json:"name,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}{}
if err := json.Unmarshal(bs, &msg); err == nil {
*m = ChatCompletionMessage(msg)
return nil
}
multiMsg := struct {
Role string `json:"role"`
Content string
Refusal string `json:"refusal,omitempty"`
MultiContent []ChatMessagePart `json:"content"`
Name string `json:"name,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}{}
if err := json.Unmarshal(bs, &multiMsg); err != nil {
return err
}
*m = ChatCompletionMessage(multiMsg)
return nil
}

View File

@@ -10,9 +10,9 @@ import (
"strconv"
"strings"
"github.com/danielmiessler/fabric/chat"
"github.com/danielmiessler/fabric/common"
"github.com/jessevdk/go-flags"
goopenai "github.com/sashabaranov/go-openai"
"golang.org/x/text/language"
"gopkg.in/yaml.v2"
)
@@ -278,22 +278,15 @@ func (o *Flags) BuildChatRequest(Meta string) (ret *common.ChatRequest, err erro
Meta: Meta,
}
var message *goopenai.ChatCompletionMessage
if len(o.Attachments) == 0 {
if o.Message != "" {
message = &goopenai.ChatCompletionMessage{
Role: goopenai.ChatMessageRoleUser,
Content: strings.TrimSpace(o.Message),
}
}
} else {
message = &goopenai.ChatCompletionMessage{
Role: goopenai.ChatMessageRoleUser,
var message *chat.ChatCompletionMessage
if len(o.Attachments) > 0 {
message = &chat.ChatCompletionMessage{
Role: chat.ChatMessageRoleUser,
}
if o.Message != "" {
message.MultiContent = append(message.MultiContent, goopenai.ChatMessagePart{
Type: goopenai.ChatMessagePartTypeText,
message.MultiContent = append(message.MultiContent, chat.ChatMessagePart{
Type: chat.ChatMessagePartTypeText,
Text: strings.TrimSpace(o.Message),
})
}
@@ -316,14 +309,20 @@ func (o *Flags) BuildChatRequest(Meta string) (ret *common.ChatRequest, err erro
dataURL := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Image)
url = &dataURL
}
message.MultiContent = append(message.MultiContent, goopenai.ChatMessagePart{
Type: goopenai.ChatMessagePartTypeImageURL,
ImageURL: &goopenai.ChatMessageImageURL{
message.MultiContent = append(message.MultiContent, chat.ChatMessagePart{
Type: chat.ChatMessagePartTypeImageURL,
ImageURL: &chat.ChatMessageImageURL{
URL: *url,
},
})
}
} else if o.Message != "" {
message = &chat.ChatCompletionMessage{
Role: chat.ChatMessageRoleUser,
Content: strings.TrimSpace(o.Message),
}
}
ret.Message = message
if o.Language != "" {

View File

@@ -1,6 +1,6 @@
package common
import goopenai "github.com/sashabaranov/go-openai"
import "github.com/danielmiessler/fabric/chat"
const ChatMessageRoleMeta = "meta"
@@ -9,7 +9,7 @@ type ChatRequest struct {
SessionName string
PatternName string
PatternVariables map[string]string
Message *goopenai.ChatCompletionMessage
Message *chat.ChatCompletionMessage
Language string
Meta string
InputHasVars bool
@@ -29,7 +29,7 @@ type ChatOptions struct {
}
// NormalizeMessages remove empty messages and ensure messages order user-assist-user
func NormalizeMessages(msgs []*goopenai.ChatCompletionMessage, defaultUserMessage string) (ret []*goopenai.ChatCompletionMessage) {
func NormalizeMessages(msgs []*chat.ChatCompletionMessage, defaultUserMessage string) (ret []*chat.ChatCompletionMessage) {
// Iterate over messages to enforce the odd position rule for user messages
fullMessageIndex := 0
for _, message := range msgs {
@@ -39,8 +39,8 @@ func NormalizeMessages(msgs []*goopenai.ChatCompletionMessage, defaultUserMessag
}
// Ensure, that each odd position shall be a user message
if fullMessageIndex%2 == 0 && message.Role != goopenai.ChatMessageRoleUser {
ret = append(ret, &goopenai.ChatCompletionMessage{Role: goopenai.ChatMessageRoleUser, Content: defaultUserMessage})
if fullMessageIndex%2 == 0 && message.Role != chat.ChatMessageRoleUser {
ret = append(ret, &chat.ChatCompletionMessage{Role: chat.ChatMessageRoleUser, Content: defaultUserMessage})
fullMessageIndex++
}
ret = append(ret, message)

View File

@@ -3,23 +3,23 @@ package common
import (
"testing"
goopenai "github.com/sashabaranov/go-openai"
"github.com/danielmiessler/fabric/chat"
"github.com/stretchr/testify/assert"
)
func TestNormalizeMessages(t *testing.T) {
msgs := []*goopenai.ChatCompletionMessage{
{Role: goopenai.ChatMessageRoleUser, Content: "Hello"},
{Role: goopenai.ChatMessageRoleAssistant, Content: "Hi there!"},
{Role: goopenai.ChatMessageRoleUser, Content: ""},
{Role: goopenai.ChatMessageRoleUser, Content: ""},
{Role: goopenai.ChatMessageRoleUser, Content: "How are you?"},
msgs := []*chat.ChatCompletionMessage{
{Role: chat.ChatMessageRoleUser, Content: "Hello"},
{Role: chat.ChatMessageRoleAssistant, Content: "Hi there!"},
{Role: chat.ChatMessageRoleUser, Content: ""},
{Role: chat.ChatMessageRoleUser, Content: ""},
{Role: chat.ChatMessageRoleUser, Content: "How are you?"},
}
expected := []*goopenai.ChatCompletionMessage{
{Role: goopenai.ChatMessageRoleUser, Content: "Hello"},
{Role: goopenai.ChatMessageRoleAssistant, Content: "Hi there!"},
{Role: goopenai.ChatMessageRoleUser, Content: "How are you?"},
expected := []*chat.ChatCompletionMessage{
{Role: chat.ChatMessageRoleUser, Content: "Hello"},
{Role: chat.ChatMessageRoleAssistant, Content: "Hi there!"},
{Role: chat.ChatMessageRoleUser, Content: "How are you?"},
}
actual := NormalizeMessages(msgs, "default")

View File

@@ -7,7 +7,7 @@ import (
"os"
"strings"
goopenai "github.com/sashabaranov/go-openai"
"github.com/danielmiessler/fabric/chat"
"github.com/danielmiessler/fabric/common"
"github.com/danielmiessler/fabric/plugins/ai"
@@ -30,11 +30,11 @@ type Chatter struct {
strategy string
}
// Send processes a chat request and applies any file changes if using the create_coding_feature pattern
// Send processes a chat request and applies file changes for create_coding_feature pattern
func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (session *fsdb.Session, err error) {
modelToUse := opts.Model
if modelToUse == "" {
modelToUse = o.model // Default to the model set in the Chatter struct
modelToUse = o.model
}
if o.vendor.NeedsRawMode(modelToUse) {
opts.Raw = true
@@ -89,18 +89,15 @@ func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (s
return
}
// Process file changes if using the create_coding_feature pattern
// Process file changes for create_coding_feature pattern
if request.PatternName == "create_coding_feature" {
// Look for file changes in the response
summary, fileChanges, parseErr := common.ParseFileChanges(message)
if parseErr != nil {
fmt.Printf("Warning: Failed to parse file changes: %v\n", parseErr)
} else if len(fileChanges) > 0 {
// Get the project root - use the current directory
projectRoot, err := os.Getwd()
if err != nil {
fmt.Printf("Warning: Failed to get current directory: %v\n", err)
// Continue without applying changes
} else {
if applyErr := common.ApplyFileChanges(projectRoot, fileChanges); applyErr != nil {
fmt.Printf("Warning: Failed to apply file changes: %v\n", applyErr)
@@ -113,7 +110,7 @@ func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (s
message = summary
}
session.Append(&goopenai.ChatCompletionMessage{Role: goopenai.ChatMessageRoleAssistant, Content: message})
session.Append(&chat.ChatCompletionMessage{Role: chat.ChatMessageRoleAssistant, Content: message})
if session.Name != "" {
err = o.db.Sessions.SaveSession(session)
@@ -122,7 +119,6 @@ func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (s
}
func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session *fsdb.Session, err error) {
// If a session name is provided, retrieve it from the database
if request.SessionName != "" {
var sess *fsdb.Session
if sess, err = o.db.Sessions.Get(request.SessionName); err != nil {
@@ -135,7 +131,7 @@ func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session *
}
if request.Meta != "" {
session.Append(&goopenai.ChatCompletionMessage{Role: common.ChatMessageRoleMeta, Content: request.Meta})
session.Append(&chat.ChatCompletionMessage{Role: common.ChatMessageRoleMeta, Content: request.Meta})
}
// if a context name is provided, retrieve it from the database
@@ -149,12 +145,12 @@ func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session *
contextContent = ctx.Content
}
// Process any template variables in the message content (user input)
// Process template variables in message content
// Double curly braces {{variable}} indicate template substitution
// Ensure we have a message before processing, other wise we'll get an error when we pass to pattern.go
// Ensure we have a message before processing
if request.Message == nil {
request.Message = &goopenai.ChatCompletionMessage{
Role: goopenai.ChatMessageRoleUser,
request.Message = &chat.ChatCompletionMessage{
Role: chat.ChatMessageRoleUser,
Content: " ",
}
}
@@ -168,19 +164,19 @@ func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session *
}
var patternContent string
inputUsed := false
if request.PatternName != "" {
pattern, err := o.db.Patterns.GetApplyVariables(request.PatternName, request.PatternVariables, request.Message.Content)
// pattern will now contain user input, and all variables will be resolved, or errored
if err != nil {
return nil, fmt.Errorf("could not get pattern %s: %v", request.PatternName, err)
}
patternContent = pattern.Pattern
inputUsed = true
}
systemMessage := strings.TrimSpace(contextContent) + strings.TrimSpace(patternContent)
// Apply strategy if specified
if request.StrategyName != "" {
strategy, err := strategy.LoadStrategy(request.StrategyName)
if err != nil {
@@ -199,33 +195,51 @@ func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session *
}
if raw {
// In raw mode, we want to avoid duplicating the input that's already in the pattern
var finalContent string
if systemMessage != "" {
// If we have a pattern, it already includes the user input
if request.PatternName != "" {
finalContent = systemMessage
} else {
// No pattern, combine system message with user input
finalContent = fmt.Sprintf("%s\n\n%s", systemMessage, request.Message.Content)
}
request.Message = &goopenai.ChatCompletionMessage{
Role: goopenai.ChatMessageRoleUser,
Content: finalContent,
// Handle MultiContent properly in raw mode
if len(request.Message.MultiContent) > 0 {
// When we have attachments, add the text as a text part in MultiContent
newMultiContent := []chat.ChatMessagePart{
{
Type: chat.ChatMessagePartTypeText,
Text: finalContent,
},
}
// Add existing non-text parts (like images)
for _, part := range request.Message.MultiContent {
if part.Type != chat.ChatMessagePartTypeText {
newMultiContent = append(newMultiContent, part)
}
}
request.Message = &chat.ChatCompletionMessage{
Role: chat.ChatMessageRoleUser,
MultiContent: newMultiContent,
}
} else {
// No attachments, use regular Content field
request.Message = &chat.ChatCompletionMessage{
Role: chat.ChatMessageRoleUser,
Content: finalContent,
}
}
}
// After this, if request.Message is not nil, append it
if request.Message != nil {
session.Append(request.Message)
}
} else { // Not raw mode
} else {
if systemMessage != "" {
session.Append(&goopenai.ChatCompletionMessage{Role: goopenai.ChatMessageRoleSystem, Content: systemMessage})
session.Append(&chat.ChatCompletionMessage{Role: chat.ChatMessageRoleSystem, Content: systemMessage})
}
// If a pattern was used (request.PatternName != ""), its output (systemMessage)
// already incorporates the user input (request.Message.Content via GetApplyVariables).
// So, we only append the direct user message if NO pattern was used.
if request.PatternName == "" && request.Message != nil {
// If multi-part content, it is in the user message, and should be added.
// Otherwise, we should only add it if we have not already used it in the systemMessage.
if len(request.Message.MultiContent) > 0 || (request.Message != nil && !inputUsed) {
session.Append(request.Message)
}
}

9
go.mod
View File

@@ -5,10 +5,11 @@ go 1.24.0
toolchain go1.24.2
require (
github.com/anaskhan96/soup v1.2.5
github.com/anthropics/anthropic-sdk-go v1.4.0
github.com/atotto/clipboard v0.1.4
github.com/aws/aws-sdk-go-v2 v1.36.4
github.com/aws/aws-sdk-go-v2/config v1.27.27
github.com/aws/aws-sdk-go-v2/service/bedrock v1.34.1
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.30.0
github.com/gabriel-vasile/mimetype v1.4.9
github.com/gin-gonic/gin v1.10.1
@@ -18,10 +19,11 @@ require (
github.com/jessevdk/go-flags v1.6.1
github.com/joho/godotenv v1.5.1
github.com/ollama/ollama v0.9.0
github.com/openai/openai-go v1.8.2
github.com/otiai10/copy v1.14.1
github.com/pkg/errors v0.9.1
github.com/samber/lo v1.50.0
github.com/sashabaranov/go-openai v1.40.1
github.com/sgaunet/perplexity-go/v2 v2.8.0
github.com/stretchr/testify v1.10.0
golang.org/x/text v0.26.0
google.golang.org/api v0.236.0
@@ -41,14 +43,12 @@ require (
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect
github.com/aws/aws-sdk-go-v2 v1.36.4 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/service/bedrock v1.34.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
@@ -92,7 +92,6 @@ require (
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/sgaunet/perplexity-go/v2 v2.8.0 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect

14
go.sum
View File

@@ -17,8 +17,6 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/anaskhan96/soup v1.2.5 h1:V/FHiusdTrPrdF4iA1YkVxsOpdNcgvqT1hG+YtcZ5hM=
github.com/anaskhan96/soup v1.2.5/go.mod h1:6YnEp9A2yywlYdM4EgDz9NEHclocMepEtku7wg6Cq3s=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@@ -31,8 +29,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
github.com/aws/aws-sdk-go-v2 v1.36.4 h1:GySzjhVvx0ERP6eyfAbAuAXLtAda5TEy19E5q5W8I9E=
github.com/aws/aws-sdk-go-v2 v1.36.4/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
@@ -43,12 +39,8 @@ github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVO
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 h1:o1v1VFfPcDVlK3ll1L5xHsaQAFdNtZ5GXnNR7SwueC4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35/go.mod h1:rZUQNYMNG+8uZxz9FOerQJ+FceCiodXvixpeRtdESrU=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 h1:R5b82ubO2NntENm3SAm0ADME+H630HomNJdgv+yZ3xw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35/go.mod h1:FuA+nmgMRfkzVKYDNEqQadvEMxtxl9+RLT9ribCwEMs=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
@@ -180,6 +172,8 @@ github.com/ollama/ollama v0.9.0 h1:GvdGhi8G/QMnFrY0TMLDy1bXua+Ify8KTkFe4ZY/OZs=
github.com/ollama/ollama v0.9.0/go.mod h1:aio9yQ7nc4uwIbn6S0LkGEPgn8/9bNQLL1nHuH+OcD0=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/openai/openai-go v1.8.2 h1:UqSkJ1vCOPUpz9Ka5tS0324EJFEuOvMc+lA/EarJWP8=
github.com/openai/openai-go v1.8.2/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
@@ -197,8 +191,6 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
github.com/sashabaranov/go-openai v1.40.1 h1:bJ08Iwct5mHBVkuvG6FEcb9MDTfsXdTYPGjYLRdeTEU=
github.com/sashabaranov/go-openai v1.40.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
@@ -213,7 +205,6 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -272,7 +263,6 @@ golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=

View File

@@ -28,9 +28,6 @@ schema = 3
[mod."github.com/ProtonMail/go-crypto"]
version = "v1.3.0"
hash = "sha256-TUG+C4MyeWglOmiwiW2/NUVurFHXLgEPRd3X9uQ1NGI="
[mod."github.com/anaskhan96/soup"]
version = "v1.2.5"
hash = "sha256-t8yCyK2y7x2qaI/3Yw16q3zVFqu+3acLcPgTr1MIKWg="
[mod."github.com/andybalholm/cascadia"]
version = "v1.3.3"
hash = "sha256-jv7ZshpSd7FZzKKN6hqlUgiR8C3y85zNIS/hq7g76Ho="
@@ -211,6 +208,9 @@ schema = 3
[mod."github.com/ollama/ollama"]
version = "v0.9.0"
hash = "sha256-r2eU+kMG3tuJy2B43RXsfmeltzM9t05NEmNiJAW5qr4="
[mod."github.com/openai/openai-go"]
version = "v1.8.2"
hash = "sha256-O8aV3zEj6o8kIlzlkYaTW4RzvwR3qNUBYiN8SuTM1R0="
[mod."github.com/otiai10/copy"]
version = "v1.14.1"
hash = "sha256-8RR7u17SbYg9AeBXVHIv5ZMU+kHmOcx0rLUKyz6YtU0="
@@ -232,9 +232,6 @@ schema = 3
[mod."github.com/samber/lo"]
version = "v1.50.0"
hash = "sha256-KDFks82BKu39sGt0f972IyOkohV2U0r1YvsnlNLdugY="
[mod."github.com/sashabaranov/go-openai"]
version = "v1.40.1"
hash = "sha256-GkToonIIF3GG+lwev1lJQ9rAAPJDjSaOkoXRC3OOlEA="
[mod."github.com/sergi/go-diff"]
version = "v1.4.0"
hash = "sha256-rs9NKpv/qcQEMRg7CmxGdP4HGuFdBxlpWf9LbA9wS4k="

View File

@@ -1 +1 @@
"1.4.214"
"1.4.224"

View File

@@ -22,19 +22,20 @@ Take a deep breath and think step by step about how to best accomplish this goal
This must be under the heading "INSIGHTFULNESS SCORE (0 = not very interesting and insightful to 10 = very interesting and insightful)".
- A rating of how emotional the debate was from 0 (very calm) to 5 (very emotional). This must be under the heading "EMOTIONALITY SCORE (0 (very calm) to 5 (very emotional))".
- A list of the participants of the debate and a score of their emotionality from 0 (very calm) to 5 (very emotional). This must be under the heading "PARTICIPANTS".
- A list of arguments attributed to participants with names and quotes. If possible, this should include external references that disprove or back up their claims.
- A list of arguments attributed to participants with names and quotes. Each argument summary must be EXACTLY 16 words. If possible, this should include external references that disprove or back up their claims.
It is IMPORTANT that these references are from trusted and verifiable sources that can be easily accessed. These sources have to BE REAL and NOT MADE UP. This must be under the heading "ARGUMENTS".
If possible, provide an objective assessment of the truth of these arguments. If you assess the truth of the argument, provide some sources that back up your assessment. The material you provide should be from reliable, verifiable, and trustworthy sources. DO NOT MAKE UP SOURCES.
- A list of agreements the participants have reached, attributed with names and quotes. This must be under the heading "AGREEMENTS".
- A list of disagreements the participants were unable to resolve and the reasons why they remained unresolved, attributed with names and quotes. This must be under the heading "DISAGREEMENTS".
- A list of possible misunderstandings and why they may have occurred, attributed with names and quotes. This must be under the heading "POSSIBLE MISUNDERSTANDINGS".
- A list of learnings from the debate. This must be under the heading "LEARNINGS".
- A list of takeaways that highlight ideas to think about, sources to explore, and actionable items. This must be under the heading "TAKEAWAYS".
- A list of agreements the participants have reached. Each agreement summary must be EXACTLY 16 words, followed by names and quotes. This must be under the heading "AGREEMENTS".
- A list of disagreements the participants were unable to resolve. Each disagreement summary must be EXACTLY 16 words, followed by names and quotes explaining why they remained unresolved. This must be under the heading "DISAGREEMENTS".
- A list of possible misunderstandings. Each misunderstanding summary must be EXACTLY 16 words, followed by names and quotes explaining why they may have occurred. This must be under the heading "POSSIBLE MISUNDERSTANDINGS".
- A list of learnings from the debate. Each learning must be EXACTLY 16 words. This must be under the heading "LEARNINGS".
- A list of takeaways that highlight ideas to think about, sources to explore, and actionable items. Each takeaway must be EXACTLY 16 words. This must be under the heading "TAKEAWAYS".
# OUTPUT INSTRUCTIONS
- Output all sections above.
- Use Markdown to structure your output.
- Do not use any markdown formatting (no asterisks, no bullet points, no headers).
- Keep all agreements, arguments, recommendations, learnings, and takeaways to EXACTLY 16 words each.
- When providing quotes, these quotes should clearly express the points you are using them for. If necessary, use multiple quotes.
# INPUT:

View File

@@ -0,0 +1,16 @@
# IDENTITY
You're an expert at finding Alpha in content.
# PHILOSOPHY
I love the idea of Claude Shannon's information theory where basically the only real information is the stuff that's different and anything that's the same as kind of background noise.
I love that idea for novelty and surprise inside of content when I think about a presentation or a talk or a podcast or an essay or anything I'm looking for the net new ideas or the new presentation of ideas for the new frameworks of how to use ideas or combine ideas so I'm looking for a way to capture that inside of content.
# INSTRUCTIONS
I want you to extract the 24 highest alpha ideas and thoughts and insights and recommendations in this piece of content, and I want you to output them in unformatted marked down in 8-word bullets written in the approachable style of Paul Graham.
# INPUT

View File

@@ -0,0 +1,64 @@
# IDENTITY and PURPOSE
You are an expert at analyzing content related to MCP (Model Context Protocol) servers. You excel at identifying and extracting mentions of MCP servers, their features, capabilities, integrations, and usage patterns.
Take a step back and think step-by-step about how to achieve the best results for extracting MCP server information.
# STEPS
- Read and analyze the entire content carefully
- Identify all mentions of MCP servers, including:
- Specific MCP server names
- Server capabilities and features
- Integration details
- Configuration examples
- Use cases and applications
- Installation or setup instructions
- API endpoints or methods exposed
- Any limitations or requirements
# OUTPUT SECTIONS
- Output a summary of all MCP servers mentioned with the following sections:
## SERVERS FOUND
- List each MCP server found with a 15-word description
- Include the server name and its primary purpose
- Use bullet points for each server
## SERVER DETAILS
For each server found, provide:
- **Server Name**: The official name
- **Purpose**: Main functionality in 25 words or less
- **Key Features**: Up to 5 main features as bullet points
- **Integration**: How it integrates with systems (if mentioned)
- **Configuration**: Any configuration details mentioned
- **Requirements**: Dependencies or requirements (if specified)
## USAGE EXAMPLES
- Extract any code snippets or usage examples
- Include configuration files or setup instructions
- Present each example with context
## INSIGHTS
- Provide 3-5 insights about the MCP servers mentioned
- Focus on patterns, trends, or notable characteristics
- Each insight should be a 20-word bullet point
# OUTPUT INSTRUCTIONS
- Output in clean, readable Markdown
- Use proper heading hierarchy
- Include code blocks with appropriate language tags
- Do not include warnings or notes about the content
- If no MCP servers are found, simply state "No MCP servers mentioned in the content"
- Ensure all server names are accurately captured
- Preserve technical details and specifications
# INPUT:
INPUT:

View File

@@ -0,0 +1,140 @@
# Code Review Task
## ROLE AND GOAL
You are a Principal Software Engineer, renowned for your meticulous attention to detail and your ability to provide clear, constructive, and educational code reviews. Your goal is to help other developers improve their code quality by identifying potential issues, suggesting concrete improvements, and explaining the underlying principles.
## TASK
You will be given a snippet of code or a diff. Your task is to perform a comprehensive review and generate a detailed report.
## STEPS
1. **Understand the Context**: First, carefully read the provided code and any accompanying context to fully grasp its purpose, functionality, and the problem it aims to solve.
2. **Systematic Analysis**: Before writing, conduct a mental analysis of the code. Evaluate it against the following key aspects. Do not write this analysis in the output; use it to form your review.
* **Correctness**: Are there bugs, logic errors, or race conditions?
* **Security**: Are there any potential vulnerabilities (e.g., injection attacks, improper handling of sensitive data)?
* **Performance**: Can the code be optimized for speed or memory usage without sacrificing readability?
* **Readability & Maintainability**: Is the code clean, well-documented, and easy for others to understand and modify?
* **Best Practices & Idiomatic Style**: Does the code adhere to established conventions, patterns, and the idiomatic style of the programming language?
* **Error Handling & Edge Cases**: Are errors handled gracefully? Have all relevant edge cases been considered?
3. **Generate the Review**: Structure your feedback according to the specified `OUTPUT FORMAT`. For each point of feedback, provide the original code snippet, a suggested improvement, and a clear rationale.
## OUTPUT FORMAT
Your review must be in Markdown and follow this exact structure:
---
### Overall Assessment
A brief, high-level summary of the code's quality. Mention its strengths and the primary areas for improvement.
### **Prioritized Recommendations**
A numbered list of the most important changes, ordered from most to least critical.
1. (Most critical change)
2. (Second most critical change)
3. ...
### **Detailed Feedback**
For each issue you identified, provide a detailed breakdown in the following format.
---
**[ISSUE TITLE]** - (e.g., `Security`, `Readability`, `Performance`)
**Original Code:**
```[language]
// The specific lines of code with the issue
```
**Suggested Improvement:**
```[language]
// The revised, improved code
```
**Rationale:**
A clear and concise explanation of why the change is recommended. Reference best practices, design patterns, or potential risks. If you use advanced concepts, briefly explain them.
---
(Repeat this section for each issue)
## EXAMPLE
Here is an example of a review for a simple Python function:
---
### **Overall Assessment**
The function correctly fetches user data, but it can be made more robust and efficient. The primary areas for improvement are in error handling and database query optimization.
### **Prioritized Recommendations**
1. Avoid making database queries inside a loop to prevent performance issues (N+1 query problem).
2. Add specific error handling for when a user is not found.
### **Detailed Feedback**
---
**[PERFORMANCE]** - N+1 Database Query
**Original Code:**
```python
def get_user_emails(user_ids):
emails = []
for user_id in user_ids:
user = db.query(User).filter(User.id == user_id).one()
emails.append(user.email)
return emails
```
**Suggested Improvement:**
```python
def get_user_emails(user_ids):
if not user_ids:
return []
users = db.query(User).filter(User.id.in_(user_ids)).all()
return [user.email for user in users]
```
**Rationale:**
The original code executes one database query for each `user_id` in the list. This is known as the "N+1 query problem" and performs very poorly on large lists. The suggested improvement fetches all users in a single query using `IN`, which is significantly more efficient.
---
**[CORRECTNESS]** - Lacks Specific Error Handling
**Original Code:**
```python
user = db.query(User).filter(User.id == user_id).one()
```
**Suggested Improvement:**
```python
from sqlalchemy.orm.exc import NoResultFound
try:
user = db.query(User).filter(User.id == user_id).one()
except NoResultFound:
# Handle the case where the user doesn't exist
# e.g., log a warning, skip the user, or raise a custom exception
continue
```
**Rationale:**
The `.one()` method will raise a `NoResultFound` exception if a user with the given ID doesn't exist, which would crash the entire function. It's better to explicitly handle this case using a try/except block to make the function more resilient.
---
## INPUT

View File

@@ -9,9 +9,9 @@ import (
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
"github.com/danielmiessler/fabric/chat"
"github.com/danielmiessler/fabric/common"
"github.com/danielmiessler/fabric/plugins"
goopenai "github.com/sashabaranov/go-openai"
)
const defaultBaseUrl = "https://api.anthropic.com/"
@@ -87,7 +87,7 @@ func (an *Client) ListModels() (ret []string, err error) {
}
func (an *Client) SendStream(
msgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions, channel chan string,
msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions, channel chan string,
) (err error) {
messages := an.toMessages(msgs)
if len(messages) == 0 {
@@ -151,7 +151,7 @@ func (an *Client) buildMessageParams(msgs []anthropic.MessageParam, opts *common
return
}
func (an *Client) Send(ctx context.Context, msgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions) (
func (an *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions) (
ret string, err error) {
messages := an.toMessages(msgs)
@@ -176,7 +176,7 @@ func (an *Client) Send(ctx context.Context, msgs []*goopenai.ChatCompletionMessa
return
}
func (an *Client) toMessages(msgs []*goopenai.ChatCompletionMessage) (ret []anthropic.MessageParam) {
func (an *Client) toMessages(msgs []*chat.ChatCompletionMessage) (ret []anthropic.MessageParam) {
// Custom normalization for Anthropic:
// - System messages become the first part of the first user message.
// - Messages must alternate user/assistant.
@@ -193,14 +193,14 @@ func (an *Client) toMessages(msgs []*goopenai.ChatCompletionMessage) (ret []anth
}
switch msg.Role {
case goopenai.ChatMessageRoleSystem:
case chat.ChatMessageRoleSystem:
// Accumulate system content. It will be prepended to the first user message.
if systemContent != "" {
systemContent += "\\n" + msg.Content
} else {
systemContent = msg.Content
}
case goopenai.ChatMessageRoleUser:
case chat.ChatMessageRoleUser:
userContent := msg.Content
if isFirstUserMessage && systemContent != "" {
userContent = systemContent + "\\n\\n" + userContent
@@ -213,7 +213,7 @@ func (an *Client) toMessages(msgs []*goopenai.ChatCompletionMessage) (ret []anth
}
anthropicMessages = append(anthropicMessages, anthropic.NewUserMessage(anthropic.NewTextBlock(userContent)))
lastRoleWasUser = true
case goopenai.ChatMessageRoleAssistant:
case chat.ChatMessageRoleAssistant:
// If the first message is an assistant message, and we have system content,
// prepend a user message with the system content.
if isFirstUserMessage && systemContent != "" {

View File

@@ -5,7 +5,8 @@ import (
"github.com/danielmiessler/fabric/plugins"
"github.com/danielmiessler/fabric/plugins/ai/openai"
goopenai "github.com/sashabaranov/go-openai"
openaiapi "github.com/openai/openai-go"
"github.com/openai/openai-go/option"
)
func NewClient() (ret *Client) {
@@ -29,11 +30,15 @@ type Client struct {
func (oi *Client) configure() (err error) {
oi.apiDeployments = strings.Split(oi.ApiDeployments.Value, ",")
config := goopenai.DefaultAzureConfig(oi.ApiKey.Value, oi.ApiBaseURL.Value)
if oi.ApiVersion.Value != "" {
config.APIVersion = oi.ApiVersion.Value
opts := []option.RequestOption{option.WithAPIKey(oi.ApiKey.Value)}
if oi.ApiBaseURL.Value != "" {
opts = append(opts, option.WithBaseURL(oi.ApiBaseURL.Value))
}
oi.ApiClient = goopenai.NewClientWithConfig(config)
if oi.ApiVersion.Value != "" {
opts = append(opts, option.WithQuery("api-version", oi.ApiVersion.Value))
}
client := openaiapi.NewClient(opts...)
oi.ApiClient = &client
return
}

View File

@@ -20,7 +20,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types"
goopenai "github.com/sashabaranov/go-openai"
"github.com/danielmiessler/fabric/chat"
)
const (
@@ -154,7 +154,7 @@ func (c *BedrockClient) ListModels() ([]string, error) {
}
// SendStream sends the messages to the the Bedrock ConverseStream API
func (c *BedrockClient) SendStream(msgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions, channel chan string) (err error) {
func (c *BedrockClient) SendStream(msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions, channel chan string) (err error) {
// Ensure channel is closed on all exit paths to prevent goroutine leaks
defer func() {
if r := recover(); r != nil {
@@ -208,7 +208,7 @@ func (c *BedrockClient) SendStream(msgs []*goopenai.ChatCompletionMessage, opts
}
// Send sends the messages the Bedrock Converse API
func (c *BedrockClient) Send(ctx context.Context, msgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions) (ret string, err error) {
func (c *BedrockClient) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions) (ret string, err error) {
messages := c.toMessages(msgs)
@@ -249,12 +249,12 @@ func (c *BedrockClient) NeedsRawMode(modelName string) bool {
// Bedrock Converse Message type.
// The system role messages are mapped to the user role as they contain a mix of system messages,
// pattern content and user input.
func (c *BedrockClient) toMessages(inputMessages []*goopenai.ChatCompletionMessage) (messages []types.Message) {
func (c *BedrockClient) toMessages(inputMessages []*chat.ChatCompletionMessage) (messages []types.Message) {
for _, msg := range inputMessages {
roles := map[string]types.ConversationRole{
goopenai.ChatMessageRoleUser: types.ConversationRoleUser,
goopenai.ChatMessageRoleAssistant: types.ConversationRoleAssistant,
goopenai.ChatMessageRoleSystem: types.ConversationRoleUser,
chat.ChatMessageRoleUser: types.ConversationRoleUser,
chat.ChatMessageRoleAssistant: types.ConversationRoleAssistant,
chat.ChatMessageRoleSystem: types.ConversationRoleUser,
}
role, ok := roles[msg.Role]

View File

@@ -4,8 +4,9 @@ import (
"bytes"
"context"
"fmt"
"strings"
goopenai "github.com/sashabaranov/go-openai"
"github.com/danielmiessler/fabric/chat"
"github.com/danielmiessler/fabric/common"
"github.com/danielmiessler/fabric/plugins"
@@ -23,62 +24,77 @@ func (c *Client) ListModels() ([]string, error) {
return []string{"dry-run-model"}, nil
}
func (c *Client) SendStream(msgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions, channel chan string) error {
output := "Dry run: Would send the following request:\n\n"
func (c *Client) formatMultiContentMessage(msg *chat.ChatCompletionMessage) string {
var builder strings.Builder
if len(msg.MultiContent) > 0 {
builder.WriteString(fmt.Sprintf("%s:\n", msg.Role))
for _, part := range msg.MultiContent {
builder.WriteString(fmt.Sprintf(" - Type: %s\n", part.Type))
if part.Type == chat.ChatMessagePartTypeImageURL {
builder.WriteString(fmt.Sprintf(" Image URL: %s\n", part.ImageURL.URL))
} else {
builder.WriteString(fmt.Sprintf(" Text: %s\n", part.Text))
}
}
builder.WriteString("\n")
} else {
builder.WriteString(fmt.Sprintf("%s:\n%s\n\n", msg.Role, msg.Content))
}
return builder.String()
}
func (c *Client) formatMessages(msgs []*chat.ChatCompletionMessage) string {
var builder strings.Builder
for _, msg := range msgs {
switch msg.Role {
case goopenai.ChatMessageRoleSystem:
output += fmt.Sprintf("System:\n%s\n\n", msg.Content)
case goopenai.ChatMessageRoleAssistant:
output += fmt.Sprintf("Assistant:\n%s\n\n", msg.Content)
case goopenai.ChatMessageRoleUser:
output += fmt.Sprintf("User:\n%s\n\n", msg.Content)
case chat.ChatMessageRoleSystem:
builder.WriteString(fmt.Sprintf("System:\n%s\n\n", msg.Content))
case chat.ChatMessageRoleAssistant:
builder.WriteString(c.formatMultiContentMessage(msg))
case chat.ChatMessageRoleUser:
builder.WriteString(c.formatMultiContentMessage(msg))
default:
output += fmt.Sprintf("%s:\n%s\n\n", msg.Role, msg.Content)
builder.WriteString(fmt.Sprintf("%s:\n%s\n\n", msg.Role, msg.Content))
}
}
output += "Options:\n"
output += fmt.Sprintf("Model: %s\n", opts.Model)
output += fmt.Sprintf("Temperature: %f\n", opts.Temperature)
output += fmt.Sprintf("TopP: %f\n", opts.TopP)
output += fmt.Sprintf("PresencePenalty: %f\n", opts.PresencePenalty)
output += fmt.Sprintf("FrequencyPenalty: %f\n", opts.FrequencyPenalty)
return builder.String()
}
func (c *Client) formatOptions(opts *common.ChatOptions) string {
var builder strings.Builder
builder.WriteString("Options:\n")
builder.WriteString(fmt.Sprintf("Model: %s\n", opts.Model))
builder.WriteString(fmt.Sprintf("Temperature: %f\n", opts.Temperature))
builder.WriteString(fmt.Sprintf("TopP: %f\n", opts.TopP))
builder.WriteString(fmt.Sprintf("PresencePenalty: %f\n", opts.PresencePenalty))
builder.WriteString(fmt.Sprintf("FrequencyPenalty: %f\n", opts.FrequencyPenalty))
if opts.ModelContextLength != 0 {
output += fmt.Sprintf("ModelContextLength: %d\n", opts.ModelContextLength)
builder.WriteString(fmt.Sprintf("ModelContextLength: %d\n", opts.ModelContextLength))
}
channel <- output
return builder.String()
}
func (c *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions, channel chan string) error {
var builder strings.Builder
builder.WriteString("Dry run: Would send the following request:\n\n")
builder.WriteString(c.formatMessages(msgs))
builder.WriteString(c.formatOptions(opts))
channel <- builder.String()
close(channel)
return nil
}
func (c *Client) Send(_ context.Context, msgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions) (string, error) {
func (c *Client) Send(_ context.Context, msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions) (string, error) {
fmt.Println("Dry run: Would send the following request:")
for _, msg := range msgs {
switch msg.Role {
case goopenai.ChatMessageRoleSystem:
fmt.Printf("System:\n%s\n\n", msg.Content)
case goopenai.ChatMessageRoleAssistant:
fmt.Printf("Assistant:\n%s\n\n", msg.Content)
case goopenai.ChatMessageRoleUser:
fmt.Printf("User:\n%s\n\n", msg.Content)
default:
fmt.Printf("%s:\n%s\n\n", msg.Role, msg.Content)
}
}
fmt.Println("Options:")
fmt.Printf("Model: %s\n", opts.Model)
fmt.Printf("Temperature: %f\n", opts.Temperature)
fmt.Printf("TopP: %f\n", opts.TopP)
fmt.Printf("PresencePenalty: %f\n", opts.PresencePenalty)
fmt.Printf("FrequencyPenalty: %f\n", opts.FrequencyPenalty)
if opts.ModelContextLength != 0 {
fmt.Printf("ModelContextLength: %d\n", opts.ModelContextLength)
}
fmt.Print(c.formatMessages(msgs))
fmt.Print(c.formatOptions(opts))
return "", nil
}

View File

@@ -4,8 +4,8 @@ import (
"reflect"
"testing"
"github.com/danielmiessler/fabric/chat"
"github.com/danielmiessler/fabric/common"
"github.com/sashabaranov/go-openai"
)
// Test generated using Keploy
@@ -33,7 +33,7 @@ func TestSetup_ReturnsNil(t *testing.T) {
// Test generated using Keploy
func TestSendStream_SendsMessages(t *testing.T) {
client := NewClient()
msgs := []*openai.ChatCompletionMessage{
msgs := []*chat.ChatCompletionMessage{
{Role: "user", Content: "Test message"},
}
opts := &common.ChatOptions{

View File

@@ -5,8 +5,8 @@ import (
"github.com/danielmiessler/fabric/plugins"
"github.com/danielmiessler/fabric/plugins/ai/openai"
goopenai "github.com/sashabaranov/go-openai"
openaiapi "github.com/openai/openai-go"
"github.com/openai/openai-go/option"
)
func NewClient() (ret *Client) {
@@ -32,10 +32,12 @@ type Client struct {
func (oi *Client) configure() (err error) {
oi.apiModels = strings.Split(oi.ApiModels.Value, ",")
config := goopenai.DefaultConfig("")
config.BaseURL = oi.ApiBaseURL.Value
oi.ApiClient = goopenai.NewClientWithConfig(config)
opts := []option.RequestOption{option.WithAPIKey(oi.ApiKey.Value)}
if oi.ApiBaseURL.Value != "" {
opts = append(opts, option.WithBaseURL(oi.ApiBaseURL.Value))
}
client := openaiapi.NewClient(opts...)
oi.ApiClient = &client
return
}

View File

@@ -6,8 +6,8 @@ import (
"fmt"
"strings"
"github.com/danielmiessler/fabric/chat"
"github.com/danielmiessler/fabric/plugins"
goopenai "github.com/sashabaranov/go-openai"
"github.com/danielmiessler/fabric/common"
"github.com/google/generative-ai-go/genai"
@@ -60,7 +60,7 @@ func (o *Client) ListModels() (ret []string, err error) {
return
}
func (o *Client) Send(ctx context.Context, msgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions) (ret string, err error) {
func (o *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions) (ret string, err error) {
systemInstruction, messages := toMessages(msgs)
var client *genai.Client
@@ -91,7 +91,7 @@ func (o *Client) buildModelNameFull(modelName string) string {
return fmt.Sprintf("%v%v", modelsNamePrefix, modelName)
}
func (o *Client) SendStream(msgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions, channel chan string) (err error) {
func (o *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions, channel chan string) (err error) {
ctx := context.Background()
var client *genai.Client
if client, err = genai.NewClient(ctx, option.WithAPIKey(o.ApiKey.Value)); err != nil {
@@ -147,7 +147,7 @@ func (o *Client) NeedsRawMode(modelName string) bool {
return false
}
func toMessages(msgs []*goopenai.ChatCompletionMessage) (systemInstruction *genai.Content, messages []genai.Part) {
func toMessages(msgs []*chat.ChatCompletionMessage) (systemInstruction *genai.Content, messages []genai.Part) {
if len(msgs) >= 2 {
systemInstruction = &genai.Content{
Parts: []genai.Part{

View File

@@ -9,7 +9,7 @@ import (
"io"
"net/http"
goopenai "github.com/sashabaranov/go-openai"
"github.com/danielmiessler/fabric/chat"
"github.com/danielmiessler/fabric/common"
"github.com/danielmiessler/fabric/plugins"
@@ -87,7 +87,7 @@ func (c *Client) ListModels() ([]string, error) {
return models, nil
}
func (c *Client) SendStream(msgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions, channel chan string) (err error) {
func (c *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions, channel chan string) (err error) {
url := fmt.Sprintf("%s/chat/completions", c.ApiUrl.Value)
payload := map[string]interface{}{
@@ -173,7 +173,7 @@ func (c *Client) SendStream(msgs []*goopenai.ChatCompletionMessage, opts *common
return
}
func (c *Client) Send(ctx context.Context, msgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions) (content string, err error) {
func (c *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions) (content string, err error) {
url := fmt.Sprintf("%s/chat/completions", c.ApiUrl.Value)
payload := map[string]interface{}{

View File

@@ -8,9 +8,9 @@ import (
"strings"
"time"
"github.com/danielmiessler/fabric/chat"
ollamaapi "github.com/ollama/ollama/api"
"github.com/samber/lo"
goopenai "github.com/sashabaranov/go-openai"
"github.com/danielmiessler/fabric/common"
"github.com/danielmiessler/fabric/plugins"
@@ -97,7 +97,7 @@ func (o *Client) ListModels() (ret []string, err error) {
return
}
func (o *Client) SendStream(msgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions, channel chan string) (err error) {
func (o *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions, channel chan string) (err error) {
req := o.createChatRequest(msgs, opts)
respFunc := func(resp ollamaapi.ChatResponse) (streamErr error) {
@@ -115,7 +115,7 @@ func (o *Client) SendStream(msgs []*goopenai.ChatCompletionMessage, opts *common
return
}
func (o *Client) Send(ctx context.Context, msgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions) (ret string, err error) {
func (o *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions) (ret string, err error) {
bf := false
req := o.createChatRequest(msgs, opts)
@@ -132,8 +132,8 @@ func (o *Client) Send(ctx context.Context, msgs []*goopenai.ChatCompletionMessag
return
}
func (o *Client) createChatRequest(msgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions) (ret ollamaapi.ChatRequest) {
messages := lo.Map(msgs, func(message *goopenai.ChatCompletionMessage, _ int) (ret ollamaapi.Message) {
func (o *Client) createChatRequest(msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions) (ret ollamaapi.ChatRequest) {
messages := lo.Map(msgs, func(message *chat.ChatCompletionMessage, _ int) (ret ollamaapi.Message) {
return ollamaapi.Message{Role: message.Role, Content: message.Content}
})

View File

@@ -0,0 +1,116 @@
package openai
// This file contains helper methods for the Chat Completions API.
// These methods are used as fallbacks for OpenAI-compatible providers
// that don't support the newer Responses API (e.g., Groq, Mistral, etc.).
import (
"context"
"strings"
"github.com/danielmiessler/fabric/chat"
"github.com/danielmiessler/fabric/common"
openai "github.com/openai/openai-go"
"github.com/openai/openai-go/shared"
)
// sendChatCompletions sends a request using the Chat Completions API
func (o *Client) sendChatCompletions(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions) (ret string, err error) {
req := o.buildChatCompletionParams(msgs, opts)
var resp *openai.ChatCompletion
if resp, err = o.ApiClient.Chat.Completions.New(ctx, req); err != nil {
return
}
if len(resp.Choices) > 0 {
ret = resp.Choices[0].Message.Content
}
return
}
// sendStreamChatCompletions sends a streaming request using the Chat Completions API
func (o *Client) sendStreamChatCompletions(
msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions, channel chan string,
) (err error) {
defer close(channel)
req := o.buildChatCompletionParams(msgs, opts)
stream := o.ApiClient.Chat.Completions.NewStreaming(context.Background(), req)
for stream.Next() {
chunk := stream.Current()
if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" {
channel <- chunk.Choices[0].Delta.Content
}
}
if stream.Err() == nil {
channel <- "\n"
}
return stream.Err()
}
// buildChatCompletionParams builds parameters for the Chat Completions API
func (o *Client) buildChatCompletionParams(
inputMsgs []*chat.ChatCompletionMessage, opts *common.ChatOptions,
) (ret openai.ChatCompletionNewParams) {
messages := make([]openai.ChatCompletionMessageParamUnion, len(inputMsgs))
for i, msgPtr := range inputMsgs {
msg := *msgPtr
if strings.Contains(opts.Model, "deepseek") && len(inputMsgs) == 1 && msg.Role == chat.ChatMessageRoleSystem {
msg.Role = chat.ChatMessageRoleUser
}
messages[i] = o.convertChatMessage(msg)
}
ret = openai.ChatCompletionNewParams{
Model: shared.ChatModel(opts.Model),
Messages: messages,
}
if !opts.Raw {
ret.Temperature = openai.Float(opts.Temperature)
ret.TopP = openai.Float(opts.TopP)
if opts.MaxTokens != 0 {
ret.MaxTokens = openai.Int(int64(opts.MaxTokens))
}
if opts.PresencePenalty != 0 {
ret.PresencePenalty = openai.Float(opts.PresencePenalty)
}
if opts.FrequencyPenalty != 0 {
ret.FrequencyPenalty = openai.Float(opts.FrequencyPenalty)
}
if opts.Seed != 0 {
ret.Seed = openai.Int(int64(opts.Seed))
}
}
return
}
// convertChatMessage converts fabric chat message to OpenAI chat completion message
func (o *Client) convertChatMessage(msg chat.ChatCompletionMessage) openai.ChatCompletionMessageParamUnion {
result := convertMessageCommon(msg)
switch result.Role {
case chat.ChatMessageRoleSystem:
return openai.SystemMessage(result.Content)
case chat.ChatMessageRoleUser:
// Handle multi-content messages (text + images)
if result.HasMultiContent {
var parts []openai.ChatCompletionContentPartUnionParam
for _, p := range result.MultiContent {
switch p.Type {
case chat.ChatMessagePartTypeText:
parts = append(parts, openai.TextContentPart(p.Text))
case chat.ChatMessagePartTypeImageURL:
parts = append(parts, openai.ImageContentPart(openai.ChatCompletionContentPartImageImageURLParam{URL: p.ImageURL.URL}))
}
}
return openai.UserMessage(parts)
}
return openai.UserMessage(result.Content)
case chat.ChatMessageRoleAssistant:
return openai.AssistantMessage(result.Content)
default:
return openai.UserMessage(result.Content)
}
}

View File

@@ -0,0 +1,21 @@
package openai
import "github.com/danielmiessler/fabric/chat"
// MessageConversionResult holds the common conversion result
type MessageConversionResult struct {
Role string
Content string
MultiContent []chat.ChatMessagePart
HasMultiContent bool
}
// convertMessageCommon extracts common conversion logic
func convertMessageCommon(msg chat.ChatCompletionMessage) MessageConversionResult {
return MessageConversionResult{
Role: msg.Role,
Content: msg.Content,
MultiContent: msg.MultiContent,
HasMultiContent: len(msg.MultiContent) > 0,
}
}

View File

@@ -2,19 +2,22 @@ package openai
import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"slices"
"strings"
"github.com/danielmiessler/fabric/chat"
"github.com/danielmiessler/fabric/common"
"github.com/danielmiessler/fabric/plugins"
goopenai "github.com/sashabaranov/go-openai"
openai "github.com/openai/openai-go"
"github.com/openai/openai-go/option"
"github.com/openai/openai-go/packages/pagination"
"github.com/openai/openai-go/responses"
"github.com/openai/openai-go/shared"
"github.com/openai/openai-go/shared/constant"
)
func NewClient() (ret *Client) {
return NewClientCompatible("OpenAI", "https://api.openai.com/v1", nil)
return NewClientCompatibleWithResponses("OpenAI", "https://api.openai.com/v1", true, nil)
}
func NewClientCompatible(vendorName string, defaultBaseUrl string, configureCustom func() error) (ret *Client) {
@@ -27,6 +30,17 @@ func NewClientCompatible(vendorName string, defaultBaseUrl string, configureCust
return
}
func NewClientCompatibleWithResponses(vendorName string, defaultBaseUrl string, implementsResponses bool, configureCustom func() error) (ret *Client) {
ret = NewClientCompatibleNoSetupQuestions(vendorName, configureCustom)
ret.ApiKey = ret.AddSetupQuestion("API Key", true)
ret.ApiBaseURL = ret.AddSetupQuestion("API Base URL", false)
ret.ApiBaseURL.Value = defaultBaseUrl
ret.ImplementsResponses = implementsResponses
return
}
func NewClientCompatibleNoSetupQuestions(vendorName string, configureCustom func() error) (ret *Client) {
ret = &Client{}
@@ -45,82 +59,87 @@ func NewClientCompatibleNoSetupQuestions(vendorName string, configureCustom func
type Client struct {
*plugins.PluginBase
ApiKey *plugins.SetupQuestion
ApiBaseURL *plugins.SetupQuestion
ApiClient *goopenai.Client
ApiKey *plugins.SetupQuestion
ApiBaseURL *plugins.SetupQuestion
ApiClient *openai.Client
ImplementsResponses bool // Whether this provider supports the Responses API
}
func (o *Client) configure() (ret error) {
config := goopenai.DefaultConfig(o.ApiKey.Value)
opts := []option.RequestOption{option.WithAPIKey(o.ApiKey.Value)}
if o.ApiBaseURL.Value != "" {
config.BaseURL = o.ApiBaseURL.Value
opts = append(opts, option.WithBaseURL(o.ApiBaseURL.Value))
}
o.ApiClient = goopenai.NewClientWithConfig(config)
client := openai.NewClient(opts...)
o.ApiClient = &client
return
}
func (o *Client) ListModels() (ret []string, err error) {
var models goopenai.ModelsList
if models, err = o.ApiClient.ListModels(context.Background()); err != nil {
var page *pagination.Page[openai.Model]
if page, err = o.ApiClient.Models.List(context.Background()); err != nil {
return
}
model := models.Models
for _, mod := range model {
for _, mod := range page.Data {
ret = append(ret, mod.ID)
}
return
}
func (o *Client) SendStream(
msgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions, channel chan string,
msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions, channel chan string,
) (err error) {
req := o.buildChatCompletionRequest(msgs, opts)
req.Stream = true
var stream *goopenai.ChatCompletionStream
if stream, err = o.ApiClient.CreateChatCompletionStream(context.Background(), req); err != nil {
fmt.Printf("ChatCompletionStream error: %v\n", err)
return
// Use Responses API for OpenAI, Chat Completions API for other providers
if o.supportsResponsesAPI() {
return o.sendStreamResponses(msgs, opts, channel)
}
return o.sendStreamChatCompletions(msgs, opts, channel)
}
defer stream.Close()
func (o *Client) sendStreamResponses(
msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions, channel chan string,
) (err error) {
defer close(channel)
for {
var response goopenai.ChatCompletionStreamResponse
if response, err = stream.Recv(); err == nil {
if len(response.Choices) > 0 {
channel <- response.Choices[0].Delta.Content
} else {
channel <- "\n"
close(channel)
break
}
} else if errors.Is(err, io.EOF) {
channel <- "\n"
close(channel)
err = nil
break
} else if err != nil {
fmt.Printf("\nStream error: %v\n", err)
break
req := o.buildResponseParams(msgs, opts)
stream := o.ApiClient.Responses.NewStreaming(context.Background(), req)
for stream.Next() {
event := stream.Current()
switch event.Type {
case string(constant.ResponseOutputTextDelta("").Default()):
channel <- event.AsResponseOutputTextDelta().Delta
case string(constant.ResponseOutputTextDone("").Default()):
channel <- event.AsResponseOutputTextDone().Text
}
}
if stream.Err() == nil {
channel <- "\n"
}
return stream.Err()
}
func (o *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions) (ret string, err error) {
// Use Responses API for OpenAI, Chat Completions API for other providers
if o.supportsResponsesAPI() {
return o.sendResponses(ctx, msgs, opts)
}
return o.sendChatCompletions(ctx, msgs, opts)
}
func (o *Client) sendResponses(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions) (ret string, err error) {
req := o.buildResponseParams(msgs, opts)
var resp *responses.Response
if resp, err = o.ApiClient.Responses.New(ctx, req); err != nil {
return
}
ret = o.extractText(resp)
return
}
func (o *Client) Send(ctx context.Context, msgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions) (ret string, err error) {
req := o.buildChatCompletionRequest(msgs, opts)
var resp goopenai.ChatCompletionResponse
if resp, err = o.ApiClient.CreateChatCompletion(ctx, req); err != nil {
return
}
if len(resp.Choices) > 0 {
ret = resp.Choices[0].Message.Content
slog.Debug("SystemFingerprint: " + resp.SystemFingerprint)
}
return
// supportsResponsesAPI determines if the provider supports the new Responses API
func (o *Client) supportsResponsesAPI() bool {
return o.ImplementsResponses
}
func (o *Client) NeedsRawMode(modelName string) bool {
@@ -129,64 +148,98 @@ func (o *Client) NeedsRawMode(modelName string) bool {
"o3",
"o4",
}
openAIModelsNeedingRaw := []string{
"gpt-4o-mini-search-preview",
"gpt-4o-mini-search-preview-2025-03-11",
"gpt-4o-search-preview",
"gpt-4o-search-preview-2025-03-11",
}
for _, prefix := range openaiModelsPrefixes {
if strings.HasPrefix(modelName, prefix) {
return true
}
}
return false
return slices.Contains(openAIModelsNeedingRaw, modelName)
}
func (o *Client) buildChatCompletionRequest(
inputMsgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions,
) (ret goopenai.ChatCompletionRequest) {
func (o *Client) buildResponseParams(
inputMsgs []*chat.ChatCompletionMessage, opts *common.ChatOptions,
) (ret responses.ResponseNewParams) {
// Create a new slice for messages to be sent, converting from []*Msg to []Msg.
// This also serves as a mutable copy for provider-specific modifications.
messagesForRequest := make([]goopenai.ChatCompletionMessage, len(inputMsgs))
items := make([]responses.ResponseInputItemUnionParam, len(inputMsgs))
for i, msgPtr := range inputMsgs {
messagesForRequest[i] = *msgPtr // Dereference and copy
msg := *msgPtr
if strings.Contains(opts.Model, "deepseek") && len(inputMsgs) == 1 && msg.Role == chat.ChatMessageRoleSystem {
msg.Role = chat.ChatMessageRoleUser
}
items[i] = convertMessage(msg)
}
// Provider-specific modification for DeepSeek:
// DeepSeek requires the last message to be a user message.
// If fabric constructs a single system message (common when a pattern includes user input),
// we change its role to user for DeepSeek.
if strings.Contains(opts.Model, "deepseek") { // Heuristic to identify DeepSeek models
if len(messagesForRequest) == 1 && messagesForRequest[0].Role == goopenai.ChatMessageRoleSystem {
messagesForRequest[0].Role = goopenai.ChatMessageRoleUser
}
// Note: This handles the most common case arising from pattern usage.
// More complex scenarios where a multi-message sequence ends in 'system'
// are not currently expected from chatter.go's BuildSession logic for OpenAI providers
// but might require further rules if they arise.
ret = responses.ResponseNewParams{
Model: shared.ResponsesModel(opts.Model),
Input: responses.ResponseNewParamsInputUnion{
OfInputItemList: items,
},
}
if opts.Raw {
ret = goopenai.ChatCompletionRequest{
Model: opts.Model,
Messages: messagesForRequest,
if !opts.Raw {
ret.Temperature = openai.Float(opts.Temperature)
ret.TopP = openai.Float(opts.TopP)
if opts.MaxTokens != 0 {
ret.MaxOutputTokens = openai.Int(int64(opts.MaxTokens))
}
} else {
if opts.Seed == 0 {
ret = goopenai.ChatCompletionRequest{
Model: opts.Model,
Temperature: float32(opts.Temperature),
TopP: float32(opts.TopP),
PresencePenalty: float32(opts.PresencePenalty),
FrequencyPenalty: float32(opts.FrequencyPenalty),
Messages: messagesForRequest,
}
} else {
ret = goopenai.ChatCompletionRequest{
Model: opts.Model,
Temperature: float32(opts.Temperature),
TopP: float32(opts.TopP),
PresencePenalty: float32(opts.PresencePenalty),
FrequencyPenalty: float32(opts.FrequencyPenalty),
Messages: messagesForRequest,
Seed: &opts.Seed,
}
// Add parameters not officially supported by Responses API as extra fields
extraFields := make(map[string]any)
if opts.PresencePenalty != 0 {
extraFields["presence_penalty"] = opts.PresencePenalty
}
if opts.FrequencyPenalty != 0 {
extraFields["frequency_penalty"] = opts.FrequencyPenalty
}
if opts.Seed != 0 {
extraFields["seed"] = opts.Seed
}
if len(extraFields) > 0 {
ret.SetExtraFields(extraFields)
}
}
return
}
func convertMessage(msg chat.ChatCompletionMessage) responses.ResponseInputItemUnionParam {
result := convertMessageCommon(msg)
role := responses.EasyInputMessageRole(result.Role)
if result.HasMultiContent {
var parts []responses.ResponseInputContentUnionParam
for _, p := range result.MultiContent {
switch p.Type {
case chat.ChatMessagePartTypeText:
parts = append(parts, responses.ResponseInputContentParamOfInputText(p.Text))
case chat.ChatMessagePartTypeImageURL:
part := responses.ResponseInputContentParamOfInputImage(responses.ResponseInputImageDetailAuto)
if part.OfInputImage != nil {
part.OfInputImage.ImageURL = openai.String(p.ImageURL.URL)
}
parts = append(parts, part)
}
}
contentList := responses.ResponseInputMessageContentListParam(parts)
return responses.ResponseInputItemParamOfMessage(contentList, role)
}
return responses.ResponseInputItemParamOfMessage(result.Content, role)
}
func (o *Client) extractText(resp *responses.Response) (ret string) {
for _, item := range resp.Output {
if item.Type == "message" {
for _, c := range item.Content {
if c.Type == "output_text" {
ret += c.AsOutputText().Text
}
}
break
}
}
return

View File

@@ -3,100 +3,60 @@ package openai
import (
"testing"
"github.com/danielmiessler/fabric/chat"
"github.com/danielmiessler/fabric/common"
"github.com/sashabaranov/go-openai"
goopenai "github.com/sashabaranov/go-openai"
openai "github.com/openai/openai-go"
"github.com/openai/openai-go/shared"
"github.com/stretchr/testify/assert"
)
func TestBuildChatCompletionRequestPinSeed(t *testing.T) {
func TestBuildResponseRequestWithMaxTokens(t *testing.T) {
var msgs []*goopenai.ChatCompletionMessage
var msgs []*chat.ChatCompletionMessage
for i := 0; i < 2; i++ {
msgs = append(msgs, &goopenai.ChatCompletionMessage{
msgs = append(msgs, &chat.ChatCompletionMessage{
Role: "User",
Content: "My msg",
})
}
opts := &common.ChatOptions{
Temperature: 0.8,
TopP: 0.9,
PresencePenalty: 0.1,
FrequencyPenalty: 0.2,
Raw: false,
Seed: 1,
}
var expectedMessages []openai.ChatCompletionMessage
for i := 0; i < 2; i++ {
expectedMessages = append(expectedMessages,
openai.ChatCompletionMessage{
Role: msgs[i].Role,
Content: msgs[i].Content,
},
)
}
var expectedRequest = goopenai.ChatCompletionRequest{
Model: opts.Model,
Temperature: float32(opts.Temperature),
TopP: float32(opts.TopP),
PresencePenalty: float32(opts.PresencePenalty),
FrequencyPenalty: float32(opts.FrequencyPenalty),
Messages: expectedMessages,
Seed: &opts.Seed,
Temperature: 0.8,
TopP: 0.9,
Raw: false,
MaxTokens: 50,
}
var client = NewClient()
request := client.buildChatCompletionRequest(msgs, opts)
assert.Equal(t, expectedRequest, request)
request := client.buildResponseParams(msgs, opts)
assert.Equal(t, shared.ResponsesModel(opts.Model), request.Model)
assert.Equal(t, openai.Float(opts.Temperature), request.Temperature)
assert.Equal(t, openai.Float(opts.TopP), request.TopP)
assert.Equal(t, openai.Int(int64(opts.MaxTokens)), request.MaxOutputTokens)
}
func TestBuildChatCompletionRequestNilSeed(t *testing.T) {
func TestBuildResponseRequestNoMaxTokens(t *testing.T) {
var msgs []*goopenai.ChatCompletionMessage
var msgs []*chat.ChatCompletionMessage
for i := 0; i < 2; i++ {
msgs = append(msgs, &goopenai.ChatCompletionMessage{
msgs = append(msgs, &chat.ChatCompletionMessage{
Role: "User",
Content: "My msg",
})
}
opts := &common.ChatOptions{
Temperature: 0.8,
TopP: 0.9,
PresencePenalty: 0.1,
FrequencyPenalty: 0.2,
Raw: false,
Seed: 0,
}
var expectedMessages []openai.ChatCompletionMessage
for i := 0; i < 2; i++ {
expectedMessages = append(expectedMessages,
openai.ChatCompletionMessage{
Role: msgs[i].Role,
Content: msgs[i].Content,
},
)
}
var expectedRequest = goopenai.ChatCompletionRequest{
Model: opts.Model,
Temperature: float32(opts.Temperature),
TopP: float32(opts.TopP),
PresencePenalty: float32(opts.PresencePenalty),
FrequencyPenalty: float32(opts.FrequencyPenalty),
Messages: expectedMessages,
Seed: nil,
Temperature: 0.8,
TopP: 0.9,
Raw: false,
}
var client = NewClient()
request := client.buildChatCompletionRequest(msgs, opts)
assert.Equal(t, expectedRequest, request)
request := client.buildResponseParams(msgs, opts)
assert.Equal(t, shared.ResponsesModel(opts.Model), request.Model)
assert.Equal(t, openai.Float(opts.Temperature), request.Temperature)
assert.Equal(t, openai.Float(opts.TopP), request.TopP)
assert.False(t, request.MaxOutputTokens.Valid())
}

View File

@@ -9,8 +9,9 @@ import (
// ProviderConfig defines the configuration for an OpenAI-compatible API provider
type ProviderConfig struct {
Name string
BaseURL string
Name string
BaseURL string
ImplementsResponses bool // Whether the provider supports OpenAI's new Responses API
}
// Client is the common structure for all OpenAI-compatible providers
@@ -21,51 +22,66 @@ type Client struct {
// NewClient creates a new OpenAI-compatible client for the specified provider
func NewClient(providerConfig ProviderConfig) *Client {
client := &Client{}
client.Client = openai.NewClientCompatible(providerConfig.Name, providerConfig.BaseURL, nil)
client.Client = openai.NewClientCompatibleWithResponses(
providerConfig.Name,
providerConfig.BaseURL,
providerConfig.ImplementsResponses,
nil,
)
return client
}
// ProviderMap is a map of provider name to ProviderConfig for O(1) lookup
var ProviderMap = map[string]ProviderConfig{
"AIML": {
Name: "AIML",
BaseURL: "https://api.aimlapi.com/v1",
Name: "AIML",
BaseURL: "https://api.aimlapi.com/v1",
ImplementsResponses: false,
},
"Cerebras": {
Name: "Cerebras",
BaseURL: "https://api.cerebras.ai/v1",
Name: "Cerebras",
BaseURL: "https://api.cerebras.ai/v1",
ImplementsResponses: false,
},
"DeepSeek": {
Name: "DeepSeek",
BaseURL: "https://api.deepseek.com",
Name: "DeepSeek",
BaseURL: "https://api.deepseek.com",
ImplementsResponses: false,
},
"GrokAI": {
Name: "GrokAI",
BaseURL: "https://api.x.ai/v1",
Name: "GrokAI",
BaseURL: "https://api.x.ai/v1",
ImplementsResponses: false,
},
"Groq": {
Name: "Groq",
BaseURL: "https://api.groq.com/openai/v1",
Name: "Groq",
BaseURL: "https://api.groq.com/openai/v1",
ImplementsResponses: false,
},
"Langdock": {
Name: "Langdock",
BaseURL: "https://api.langdock.com/openai/{{REGION=us}}/v1",
Name: "Langdock",
BaseURL: "https://api.langdock.com/openai/{{REGION=us}}/v1",
ImplementsResponses: false,
},
"LiteLLM": {
Name: "LiteLLM",
BaseURL: "http://localhost:4000",
Name: "LiteLLM",
BaseURL: "http://localhost:4000",
ImplementsResponses: false,
},
"Mistral": {
Name: "Mistral",
BaseURL: "https://api.mistral.ai/v1",
Name: "Mistral",
BaseURL: "https://api.mistral.ai/v1",
ImplementsResponses: false,
},
"OpenRouter": {
Name: "OpenRouter",
BaseURL: "https://openrouter.ai/api/v1",
Name: "OpenRouter",
BaseURL: "https://openrouter.ai/api/v1",
ImplementsResponses: false,
},
"SiliconCloud": {
Name: "SiliconCloud",
BaseURL: "https://api.siliconflow.cn/v1",
Name: "SiliconCloud",
BaseURL: "https://api.siliconflow.cn/v1",
ImplementsResponses: false,
},
}

View File

@@ -10,7 +10,7 @@ import (
"github.com/danielmiessler/fabric/plugins"
perplexity "github.com/sgaunet/perplexity-go/v2"
goopenai "github.com/sashabaranov/go-openai"
"github.com/danielmiessler/fabric/chat"
)
const (
@@ -61,7 +61,7 @@ func (c *Client) ListModels() ([]string, error) {
return models, nil
}
func (c *Client) Send(ctx context.Context, msgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions) (string, error) {
func (c *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions) (string, error) {
if c.client == nil {
if err := c.Configure(); err != nil {
return "", fmt.Errorf("failed to configure Perplexity client: %w", err)
@@ -120,7 +120,7 @@ func (c *Client) Send(ctx context.Context, msgs []*goopenai.ChatCompletionMessag
return content, nil
}
func (c *Client) SendStream(msgs []*goopenai.ChatCompletionMessage, opts *common.ChatOptions, channel chan string) error {
func (c *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions, channel chan string) error {
if c.client == nil {
if err := c.Configure(); err != nil {
close(channel) // Ensure channel is closed on error

View File

@@ -3,8 +3,8 @@ package ai
import (
"context"
"github.com/danielmiessler/fabric/chat"
"github.com/danielmiessler/fabric/plugins"
goopenai "github.com/sashabaranov/go-openai"
"github.com/danielmiessler/fabric/common"
)
@@ -12,7 +12,7 @@ import (
type Vendor interface {
plugins.Plugin
ListModels() ([]string, error)
SendStream([]*goopenai.ChatCompletionMessage, *common.ChatOptions, chan string) error
Send(context.Context, []*goopenai.ChatCompletionMessage, *common.ChatOptions) (string, error)
SendStream([]*chat.ChatCompletionMessage, *common.ChatOptions, chan string) error
Send(context.Context, []*chat.ChatCompletionMessage, *common.ChatOptions) (string, error)
NeedsRawMode(modelName string) bool
}

View File

@@ -3,8 +3,8 @@ package fsdb
import (
"fmt"
"github.com/danielmiessler/fabric/chat"
"github.com/danielmiessler/fabric/common"
goopenai "github.com/sashabaranov/go-openai"
)
type SessionsEntity struct {
@@ -38,16 +38,16 @@ func (o *SessionsEntity) SaveSession(session *Session) (err error) {
type Session struct {
Name string
Messages []*goopenai.ChatCompletionMessage
Messages []*chat.ChatCompletionMessage
vendorMessages []*goopenai.ChatCompletionMessage
vendorMessages []*chat.ChatCompletionMessage
}
func (o *Session) IsEmpty() bool {
return len(o.Messages) == 0
}
func (o *Session) Append(messages ...*goopenai.ChatCompletionMessage) {
func (o *Session) Append(messages ...*chat.ChatCompletionMessage) {
if o.vendorMessages != nil {
for _, message := range messages {
o.Messages = append(o.Messages, message)
@@ -58,7 +58,7 @@ func (o *Session) Append(messages ...*goopenai.ChatCompletionMessage) {
}
}
func (o *Session) GetVendorMessages() (ret []*goopenai.ChatCompletionMessage) {
func (o *Session) GetVendorMessages() (ret []*chat.ChatCompletionMessage) {
if len(o.vendorMessages) == 0 {
for _, message := range o.Messages {
o.appendVendorMessage(message)
@@ -68,13 +68,13 @@ func (o *Session) GetVendorMessages() (ret []*goopenai.ChatCompletionMessage) {
return
}
func (o *Session) appendVendorMessage(message *goopenai.ChatCompletionMessage) {
func (o *Session) appendVendorMessage(message *chat.ChatCompletionMessage) {
if message.Role != common.ChatMessageRoleMeta {
o.vendorMessages = append(o.vendorMessages, message)
}
}
func (o *Session) GetLastMessage() (ret *goopenai.ChatCompletionMessage) {
func (o *Session) GetLastMessage() (ret *chat.ChatCompletionMessage) {
if len(o.Messages) > 0 {
ret = o.Messages[len(o.Messages)-1]
}
@@ -86,9 +86,9 @@ func (o *Session) String() (ret string) {
ret += fmt.Sprintf("\n--- \n[%v]\n%v", message.Role, message.Content)
if message.MultiContent != nil {
for _, part := range message.MultiContent {
if part.Type == goopenai.ChatMessagePartTypeImageURL {
if part.Type == chat.ChatMessagePartTypeImageURL {
ret += fmt.Sprintf("\n%v: %v", part.Type, *part.ImageURL)
} else if part.Type == goopenai.ChatMessagePartTypeText {
} else if part.Type == chat.ChatMessagePartTypeText {
ret += fmt.Sprintf("\n%v: %v", part.Type, part.Text)
}
}

View File

@@ -3,7 +3,7 @@ package fsdb
import (
"testing"
goopenai "github.com/sashabaranov/go-openai"
"github.com/danielmiessler/fabric/chat"
)
func TestSessions_GetOrCreateSession(t *testing.T) {
@@ -27,7 +27,7 @@ func TestSessions_SaveSession(t *testing.T) {
StorageEntity: &StorageEntity{Dir: dir, FileExtension: ".json"},
}
sessionName := "testSession"
session := &Session{Name: sessionName, Messages: []*goopenai.ChatCompletionMessage{{Content: "message1"}}}
session := &Session{Name: sessionName, Messages: []*chat.ChatCompletionMessage{{Content: "message1"}}}
err := sessions.SaveSession(session)
if err != nil {
t.Fatalf("failed to save session: %v", err)

View File

@@ -93,7 +93,7 @@ func TestSysPlugin(t *testing.T) {
if !filepath.IsAbs(got) {
return fmt.Errorf("expected absolute path, got %s", got)
}
if !strings.Contains(got, "home") && !strings.Contains(got, "Users") {
if !strings.Contains(got, "home") && !strings.Contains(got, "Users") && got != "/root" {
return fmt.Errorf("path %s doesn't look like a home directory", got)
}
return nil

View File

@@ -3,14 +3,13 @@ package restapi
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"strings"
goopenai "github.com/sashabaranov/go-openai"
"github.com/danielmiessler/fabric/chat"
"github.com/danielmiessler/fabric/common"
"github.com/danielmiessler/fabric/core"
@@ -95,7 +94,7 @@ func (h *ChatHandler) HandleChat(c *gin.Context) {
// Load and prepend strategy prompt if strategyName is set
if p.StrategyName != "" {
strategyFile := filepath.Join(os.Getenv("HOME"), ".config", "fabric", "strategies", p.StrategyName+".json")
data, err := ioutil.ReadFile(strategyFile)
data, err := os.ReadFile(strategyFile)
if err == nil {
var s struct {
Prompt string `json:"prompt"`
@@ -115,7 +114,7 @@ func (h *ChatHandler) HandleChat(c *gin.Context) {
// Pass the language received in the initial request to the common.ChatRequest
chatReq := &common.ChatRequest{
Message: &goopenai.ChatCompletionMessage{
Message: &chat.ChatCompletionMessage{
Role: "user",
Content: p.UserInput,
},

View File

@@ -103,7 +103,6 @@ func ServeOllama(registry *core.PluginRegistry, address string, version string)
r.GET("/api/tags", typeConversion.ollamaTags)
r.GET("/api/version", func(c *gin.Context) {
c.Data(200, "application/json", []byte(fmt.Sprintf("{\"%s\"}", version)))
return
})
r.POST("/api/chat", typeConversion.ollamaChat)
@@ -262,15 +261,10 @@ func (f APIConvert) ollamaChat(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": err})
return
}
for _, bytein := range marshalled {
res = append(res, bytein)
}
for _, bytebreak := range []byte("\n") {
res = append(res, bytebreak)
}
res = append(res, marshalled...)
res = append(res, '\n')
}
c.Data(200, "application/json", res)
//c.JSON(200, forwardedResponse)
return
}

View File

@@ -26,6 +26,7 @@ func Serve(registry *core.PluginRegistry, address string, apiKey string) (err er
NewContextsHandler(r, fabricDb.Contexts)
NewSessionsHandler(r, fabricDb.Sessions)
NewChatHandler(r, registry, fabricDb)
NewYouTubeHandler(r, registry)
NewConfigHandler(r, fabricDb)
NewModelsHandler(r, registry.VendorManager)
NewStrategiesHandler(r)

View File

@@ -2,7 +2,6 @@ package restapi
import (
"encoding/json"
"io/ioutil"
"net/http"
"os"
"path/filepath"
@@ -23,7 +22,7 @@ func NewStrategiesHandler(r *gin.Engine) {
r.GET("/strategies", func(c *gin.Context) {
strategiesDir := filepath.Join(os.Getenv("HOME"), ".config", "fabric", "strategies")
files, err := ioutil.ReadDir(strategiesDir)
files, err := os.ReadDir(strategiesDir)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read strategies directory"})
return
@@ -37,7 +36,7 @@ func NewStrategiesHandler(r *gin.Engine) {
}
fullPath := filepath.Join(strategiesDir, file.Name())
data, err := ioutil.ReadFile(fullPath)
data, err := os.ReadFile(fullPath)
if err != nil {
continue
}

70
restapi/youtube.go Normal file
View File

@@ -0,0 +1,70 @@
package restapi
import (
"net/http"
"github.com/danielmiessler/fabric/core"
"github.com/danielmiessler/fabric/plugins/tools/youtube"
"github.com/gin-gonic/gin"
)
type YouTubeHandler struct {
yt *youtube.YouTube
}
type YouTubeRequest struct {
URL string `json:"url"`
Language string `json:"language"`
Timestamps bool `json:"timestamps"`
}
type YouTubeResponse struct {
Transcript string `json:"transcript"`
Title string `json:"title"`
}
func NewYouTubeHandler(r *gin.Engine, registry *core.PluginRegistry) *YouTubeHandler {
handler := &YouTubeHandler{yt: registry.YouTube}
r.POST("/youtube/transcript", handler.Transcript)
return handler
}
func (h *YouTubeHandler) Transcript(c *gin.Context) {
var req YouTubeRequest
if err := c.BindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
if req.URL == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "url is required"})
return
}
language := req.Language
if language == "" {
language = "en"
}
var videoID, playlistID string
var err error
if videoID, playlistID, err = h.yt.GetVideoOrPlaylistId(req.URL); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if videoID == "" && playlistID != "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "URL is a playlist, not a video"})
return
}
var transcript string
if req.Timestamps {
transcript, err = h.yt.GrabTranscriptWithTimestamps(videoID, language)
} else {
transcript, err = h.yt.GrabTranscript(videoID, language)
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, YouTubeResponse{Transcript: transcript, Title: videoID})
}

View File

@@ -1,3 +1,3 @@
package main
var version = "v1.4.214"
var version = "v1.4.224"

View File

@@ -1,5 +1,5 @@
import { get } from 'svelte/store';
import { languageStore } from '$lib/store/language-store';
import { get } from 'svelte/store';
export interface TranscriptResponse {
transcript: string;
@@ -18,18 +18,18 @@ export async function getTranscript(url: string): Promise<TranscriptResponse> {
console.log('\n=== YouTube Transcript Service Start ===');
console.log('1. Request details:', {
url,
endpoint: '/chat',
endpoint: '/api/youtube/transcript',
method: 'POST',
isYouTubeURL: url.includes('youtube.com') || url.includes('youtu.be'),
originalLanguage
});
const response = await fetch('/chat', {
const response = await fetch('/api/youtube/transcript', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
body: JSON.stringify({
url,
language: originalLanguage // Pass original language to server
})

File diff suppressed because it is too large Load Diff