Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4119b2ec41 | ||
|
|
01b7f1cd78 | ||
|
|
c606f72d90 | ||
|
|
8a64d9c959 |
@@ -3,3 +3,5 @@ __pycache__
|
||||
.venv
|
||||
.aider*
|
||||
.vscode
|
||||
.zed
|
||||
pyproject.toml
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ ENV PYTHONUNBUFFERED 1
|
||||
|
||||
ADD src/ /blog_creator
|
||||
|
||||
RUN apt-get update && apt-get install -y rustc cargo python-is-python3 pip python3-venv libmagic-dev
|
||||
RUN apt-get update && apt-get install -y rustc cargo python-is-python3 pip python3-venv libmagic-dev git
|
||||
|
||||
RUN python -m venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
@@ -3,10 +3,19 @@
|
||||
This creator requires you to use a working Trilium Instance and create a .env file with the following
|
||||
|
||||
```
|
||||
TRILIUM_HOST
|
||||
TRILIUM_PORT
|
||||
TRILIUM_PROTOCOL
|
||||
TRILIUM_PASS
|
||||
TRILIUM_HOST=
|
||||
TRILIUM_PORT=
|
||||
TRILIUM_PROTOCOL=
|
||||
TRILIUM_PASS=
|
||||
TRILIUM_TOKEN=
|
||||
OLLAMA_PROTOCOL=
|
||||
OLLAMA_HOST=
|
||||
OLLAMA_PORT=11434
|
||||
EMBEDDING_MODEL=
|
||||
EDITOR_MODEL=
|
||||
# This is expected in python list format example `[phi4-mini:latest, qwen3:1.7b, gemma3:latest]`
|
||||
CONTENT_CREATOR_MODELS=
|
||||
CHROMA_SERVER=<IP_ADDRESS>
|
||||
```
|
||||
|
||||
This container is going to be what I use to trigger a blog creation event
|
||||
|
||||
+3
-12
@@ -23,25 +23,16 @@ services:
|
||||
# Default configuration for persist_directory in chromadb/config.py
|
||||
# Read more about deployments: https://docs.trychroma.com/deployment
|
||||
- chroma-data:/chroma/chroma
|
||||
command: "--workers 1 --host 0.0.0.0 --port 8000 --proxy-headers --log-config chromadb/log_config.yml --timeout-keep-alive 30"
|
||||
#command: "--host 0.0.0.0 --port 8000 --proxy-headers --log-config chromadb/log_config.yml --timeout-keep-alive 30"
|
||||
environment:
|
||||
- IS_PERSISTENT=TRUE
|
||||
- CHROMA_SERVER_AUTHN_PROVIDER=${CHROMA_SERVER_AUTHN_PROVIDER}
|
||||
- CHROMA_SERVER_AUTHN_CREDENTIALS_FILE=${CHROMA_SERVER_AUTHN_CREDENTIALS_FILE}
|
||||
- CHROMA_SERVER_AUTHN_CREDENTIALS=${CHROMA_SERVER_AUTHN_CREDENTIALS}
|
||||
- CHROMA_AUTH_TOKEN_TRANSPORT_HEADER=${CHROMA_AUTH_TOKEN_TRANSPORT_HEADER}
|
||||
- PERSIST_DIRECTORY=${PERSIST_DIRECTORY:-/chroma/chroma}
|
||||
- CHROMA_OTEL_EXPORTER_ENDPOINT=${CHROMA_OTEL_EXPORTER_ENDPOINT}
|
||||
- CHROMA_OTEL_EXPORTER_HEADERS=${CHROMA_OTEL_EXPORTER_HEADERS}
|
||||
- CHROMA_OTEL_SERVICE_NAME=${CHROMA_OTEL_SERVICE_NAME}
|
||||
- CHROMA_OTEL_GRANULARITY=${CHROMA_OTEL_GRANULARITY}
|
||||
- CHROMA_SERVER_NOFILE=${CHROMA_SERVER_NOFILE}
|
||||
restart: unless-stopped # possible values are: "no", always", "on-failure", "unless-stopped"
|
||||
ports:
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
# Adjust below to match your container port
|
||||
test: [ "CMD", "curl", "-f", "http://localhost:8000/api/v2/heartbeat" ]
|
||||
test:
|
||||
["CMD", "curl", "-f", "http://localhost:8000/api/v2/heartbeat"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import os, re
|
||||
import os, re, json, random, time
|
||||
from ollama import Client
|
||||
import chromadb, time
|
||||
import chromadb
|
||||
from langchain_ollama import ChatOllama
|
||||
|
||||
class OllamaGenerator:
|
||||
|
||||
def __init__(self, title: str, content: str, model: str, inner_title: str):
|
||||
def __init__(self, title: str, content: str, inner_title: str):
|
||||
self.title = title
|
||||
self.inner_title = inner_title
|
||||
self.content = content
|
||||
self.chroma = chromadb.HttpClient(host="172.18.0.2", port=8000)
|
||||
self.response = None
|
||||
self.chroma = chromadb.HttpClient(host="172.19.0.2", port=8000)
|
||||
ollama_url = f"{os.environ["OLLAMA_PROTOCOL"]}://{os.environ["OLLAMA_HOST"]}:{os.environ["OLLAMA_PORT"]}"
|
||||
self.ollama_client = Client(host=ollama_url)
|
||||
self.ollama_model = model
|
||||
self.embed_model = "snowflake-arctic-embed2:latest"
|
||||
self.agent_models = ["openthinker:7b", "deepseek-r1:7b", "qwen2.5:7b", "gemma3:latest"]
|
||||
self.llm = ChatOllama(model=self.ollama_model, temperature=0.7)
|
||||
self.ollama_model = os.environ["EDITOR_MODEL"]
|
||||
self.embed_model = os.environ["EMBEDDING_MODEL"]
|
||||
self.agent_models = json.loads(os.environ["CONTENT_CREATOR_MODELS"])
|
||||
self.llm = ChatOllama(model=self.ollama_model, temperature=0.6, top_p=0.5) #This is the level head in the room
|
||||
self.prompt_inject = f"""
|
||||
You are a journalist, Software Developer and DevOps expert
|
||||
writing a 1000 word draft blog for other tech enthusiasts.
|
||||
@@ -53,12 +54,20 @@ class OllamaGenerator:
|
||||
def generate_draft(self, model) -> str:
|
||||
'''Generate a draft blog post using the specified model'''
|
||||
try:
|
||||
agent_llm = ChatOllama(model=model, temperature=0.8)
|
||||
# the idea behind this is to make the "creativity" random amongst the content creators
|
||||
# contorlling temperature will allow cause the output to allow more "random" connections in sentences
|
||||
# Controlling top_p will tighten or loosen the embedding connections made
|
||||
# The result should be varied levels of "creativity" in the writing of the drafts
|
||||
# for more see https://python.langchain.com/v0.2/api_reference/ollama/chat_models/langchain_ollama.chat_models.ChatOllama.html
|
||||
temp = random.uniform(0.5, 1.0)
|
||||
top_p = random.uniform(0.4, 0.8)
|
||||
top_k = int(random.uniform(30, 80))
|
||||
agent_llm = ChatOllama(model=model, temperature=temp, top_p=top_p, top_k=top_k)
|
||||
messages = [
|
||||
("system", self.prompt_inject),
|
||||
("human", "make the blog post in a format to be edited easily" )
|
||||
]
|
||||
self.response = agent_llm.invoke(messages)
|
||||
response = agent_llm.invoke(messages)
|
||||
# self.response = self.ollama_client.chat(model=model,
|
||||
# messages=[
|
||||
# {
|
||||
@@ -66,7 +75,9 @@ class OllamaGenerator:
|
||||
# 'content': f'{self.prompt_inject}',
|
||||
# },
|
||||
# ])
|
||||
return self.response.text()#['message']['content']
|
||||
#print ("draft")
|
||||
#print (response)
|
||||
return response.text()#['message']['content']
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to generate blog draft: {e}")
|
||||
@@ -117,6 +128,7 @@ class OllamaGenerator:
|
||||
collection_query = collection.query(query_embeddings=query_embed, n_results=100)
|
||||
print("Showing pertinent info from drafts used in final edited edition")
|
||||
pertinent_draft_info = '\n\n'.join(collection.query(query_embeddings=query_embed, n_results=100)['documents'][0])
|
||||
#print(pertinent_draft_info)
|
||||
prompt_human = f"Generate the final document using this information from the drafts: {pertinent_draft_info} - ONLY OUTPUT THE MARKDOWN"
|
||||
print("Generating final document")
|
||||
messages = [("system", prompt_system), ("human", prompt_human),]
|
||||
@@ -128,6 +140,8 @@ class OllamaGenerator:
|
||||
# 'content': f'{prompt_enhanced}',
|
||||
# },
|
||||
# ])
|
||||
#print ("Markdown Generated")
|
||||
#print (self.response)
|
||||
return self.response#['message']['content']
|
||||
|
||||
except Exception as e:
|
||||
@@ -136,3 +150,10 @@ class OllamaGenerator:
|
||||
def save_to_file(self, filename: str) -> None:
|
||||
with open(filename, "w") as f:
|
||||
f.write(self.generate_markdown())
|
||||
|
||||
def generate_commit_message(self):
|
||||
prompt_system = "You are a blog creator commiting a piece of content to a central git repo"
|
||||
prompt_human = f"Generate a 10 word git commit message describing {self.response}"
|
||||
messages = [("system", prompt_system), ("human", prompt_human),]
|
||||
commit_message = self.llm.invoke(messages).text()
|
||||
return commit_message
|
||||
+10
-3
@@ -1,6 +1,7 @@
|
||||
import ai_generators.ollama_md_generator as omg
|
||||
import trilium.notes as tn
|
||||
import string
|
||||
import repo_management.repo_manager as git_repo
|
||||
import string,os
|
||||
|
||||
tril = tn.TrilumNotes()
|
||||
|
||||
@@ -22,6 +23,12 @@ for note in tril_notes:
|
||||
os_friendly_title = convert_to_lowercase_with_underscores(tril_notes[note]['title'])
|
||||
ai_gen = omg.OllamaGenerator(os_friendly_title,
|
||||
tril_notes[note]['content'],
|
||||
"gemma3:latest",
|
||||
tril_notes[note]['title'])
|
||||
ai_gen.save_to_file(f"/blog_creator/generated_files/{os_friendly_title}.md")
|
||||
blog_path = f"/blog_creator/generated_files/{os_friendly_title}.md"
|
||||
ai_gen.save_to_file(blog_path)
|
||||
# Generate commit messages and push to repo
|
||||
commit_message = ai_gen.generate_commit_message()
|
||||
git_user = os.environp["GIT_USER"]
|
||||
git_pass = os.environ["GIT_PASS"]
|
||||
repo_manager = git_repo("blog/", git_user, git_pass)
|
||||
repo_manager.create_copy_commit_push(blog_path, os_friendly_title, commit_message)
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
from git import Repo
|
||||
|
||||
# Set these variables accordingly
|
||||
REPO_OWNER = "your_repo_owner"
|
||||
REPO_NAME = "your_repo_name"
|
||||
|
||||
def clone_repo(repo_url, branch="main"):
|
||||
Repo.clone_from(repo_url, ".", branch=branch)
|
||||
|
||||
def create_markdown_file(file_name, content):
|
||||
with open(f"{file_name}.md", "w") as f:
|
||||
f.write(content)
|
||||
|
||||
def commit_and_push(file_name, message):
|
||||
repo = Repo(".")
|
||||
repo.index.add([f"{file_name}.md"])
|
||||
repo.index.commit(message)
|
||||
repo.remote().push()
|
||||
|
||||
def create_new_branch(branch_name):
|
||||
repo = Repo(".")
|
||||
repo.create_head(branch_name).checkout()
|
||||
repo.head.reference.set_tracking_url(f"https://your_git_server/{REPO_OWNER}/{REPO_NAME}.git/{branch_name}")
|
||||
repo.remote().push()
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: python push_markdown.py <repo_url> <markdown_file_name>")
|
||||
sys.exit(1)
|
||||
|
||||
repo_url = sys.argv[1]
|
||||
file_name = sys.argv[2]
|
||||
|
||||
# Clone the repository
|
||||
clone_repo(repo_url)
|
||||
|
||||
# Create a new Markdown file with content
|
||||
create_markdown_file(file_name, "Hello, World!\n")
|
||||
|
||||
# Commit and push changes to the main branch
|
||||
commit_and_push(file_name, f"Add {file_name}.md")
|
||||
|
||||
# Create a new branch named after the Markdown file
|
||||
create_new_branch(file_name)
|
||||
|
||||
print(f"Successfully created '{file_name}' branch with '{file_name}.md'.")
|
||||
@@ -1,39 +1,91 @@
|
||||
import os
|
||||
from git import Git
|
||||
from git.repo import BaseRepository
|
||||
from git.exc import InvalidGitRepositoryError
|
||||
from git.remote import RemoteAction
|
||||
import os, shutil
|
||||
from git import Repo
|
||||
from git.exc import GitCommandError
|
||||
|
||||
class GitRepository:
|
||||
# This is designed to be transitory it will desctruvtively create the repo at repo_path
|
||||
# if you have uncommited changes you can kiss them goodbye!
|
||||
# Don't use the repo created by this function for dev -> its a tool!
|
||||
# It is expected that when used you will add, commit, push, delete
|
||||
def __init__(self, repo_path, username=None, password=None):
|
||||
git_protocol = os.environ["GIT_PROTOCOL"]
|
||||
git_remote = os.environ["GIT_REMOTE"]
|
||||
remote = f"{git_protocol}://{username}:{password}@{git_remote}"
|
||||
|
||||
if os.path.exists(repo_path):
|
||||
shutil.rmtree(repo_path)
|
||||
self.repo_path = repo_path
|
||||
Repo.clone_from(remote, repo_path)
|
||||
self.repo = Repo(repo_path)
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def clone(self, remote_url, destination_path):
|
||||
"""Clone a Git repository with authentication"""
|
||||
try:
|
||||
self.repo.clone(remote_url, destination_path)
|
||||
return True
|
||||
except GitCommandError as e:
|
||||
print(f"Cloning failed: {e}")
|
||||
return False
|
||||
|
||||
def fetch(self, remote_name='origin', ref_name='main'):
|
||||
"""Fetch updates from a remote repository with authentication"""
|
||||
try:
|
||||
self.repo.remotes[remote_name].fetch(ref_name=ref_name)
|
||||
return True
|
||||
except GitCommandError as e:
|
||||
print(f"Fetching failed: {e}")
|
||||
return False
|
||||
|
||||
def pull(self, remote_name='origin', ref_name='main'):
|
||||
"""Pull updates from a remote repository with authentication"""
|
||||
try:
|
||||
self.repo.remotes[remote_name].pull(ref_name=ref_name)
|
||||
return True
|
||||
except GitCommandError as e:
|
||||
print(f"Pulling failed: {e}")
|
||||
return False
|
||||
|
||||
def get_branches(self):
|
||||
"""List all branches in the repository"""
|
||||
return [branch.name for branch in self.repo.branches]
|
||||
|
||||
|
||||
def try_something(test):
|
||||
def create_branch(self, branch_name, remote_name='origin', ref_name='main'):
|
||||
"""Create a new branch in the repository with authentication."""
|
||||
try:
|
||||
# Use the same remote and ref as before
|
||||
self.repo.git.branch(branch_name, commit=True)
|
||||
return True
|
||||
except GitCommandError as e:
|
||||
print(f"Failed to create branch: {e}")
|
||||
return False
|
||||
|
||||
# Set the path to your blog repo here
|
||||
blog_repo = "/path/to/your/blog/repo"
|
||||
def add_and_commit(self, message=None):
|
||||
"""Add and commit changes to the repository."""
|
||||
try:
|
||||
# Add all changes
|
||||
self.repo.git.add(all=True)
|
||||
# Commit with the provided message or a default
|
||||
if message is None:
|
||||
commit_message = "Added and committed new content"
|
||||
else:
|
||||
commit_message = message
|
||||
self.repo.git.commit(commit_message=commit_message)
|
||||
return True
|
||||
except GitCommandError as e:
|
||||
print(f"Commit failed: {e}")
|
||||
return False
|
||||
|
||||
def create_copy_commit_push(self, file_path, title, commit_messge):
|
||||
self.create_branch(title)
|
||||
|
||||
# Checkout a new branch and create a new file for our blog post
|
||||
branch_name = "new-post"
|
||||
try:
|
||||
repo = Git(blog_repo)
|
||||
repo.checkout("-b", branch_name, "origin/main")
|
||||
with open("my-blog-post.md", "w") as f:
|
||||
f.write(content)
|
||||
except InvalidGitRepositoryError:
|
||||
# Handle repository errors gracefully
|
||||
pass
|
||||
shutil.copy(f"{file_path}", f"{self.repo_path}src/content/")
|
||||
|
||||
# Add and commit the changes to Git
|
||||
repo.add("my-blog-post.md")
|
||||
repo.commit("-m", "Added new blog post about DevOps best practices.")
|
||||
self.add_and_commit(commit_messge)
|
||||
|
||||
# Push the changes to Git and create a PR
|
||||
repo.remote().push("refs/heads/{0}:refs/for/main".format(branch_name), "--set-upstream")
|
||||
base_branch = "origin/main"
|
||||
target_branch = "main"
|
||||
pr_title = "DevOps best practices"
|
||||
try:
|
||||
repo.create_head("{0}-{1}", base=base_branch, message="{}".format(pr_title))
|
||||
except RemoteAction.GitExitStatus as e:
|
||||
# Handle Git exit status errors gracefully
|
||||
pass
|
||||
self.repo.git.push(remote_name='origin', ref_name=title, force=True)
|
||||
|
||||
def remove_repo(self):
|
||||
shutil.rmtree(self.repo_path)
|
||||
@@ -18,9 +18,13 @@ class TrilumNotes:
|
||||
print("Please run get_token and set your token")
|
||||
else:
|
||||
self.ea = ETAPI(self.server_url, self.token)
|
||||
self.new_notes = None
|
||||
self.note_content = None
|
||||
|
||||
def get_token(self):
|
||||
ea = ETAPI(self.server_url)
|
||||
if self.tril_pass == None:
|
||||
raise ValueError("Trillium password can not be none")
|
||||
token = ea.login(self.tril_pass)
|
||||
print(token)
|
||||
print("I would recomend you update the env file with this tootsweet!")
|
||||
@@ -40,10 +44,11 @@ class TrilumNotes:
|
||||
|
||||
def get_notes_content(self):
|
||||
content_dict = {}
|
||||
if self.new_notes is None:
|
||||
raise ValueError("How did you do this? new_notes is None!")
|
||||
for note in self.new_notes['results']:
|
||||
content_dict[note['noteId']] = {"title" : f"{note['title']}",
|
||||
"content" : f"{self._get_content(note['noteId'])}"
|
||||
}
|
||||
self.note_content = content_dict
|
||||
return content_dict
|
||||
|
||||
|
||||
Reference in New Issue
Block a user