feat: add heroprompt module for prompt generation

- Introduce sessions and workspaces for managing context
- Allow adding directories and selecting files
- Generate structured prompts with file maps and content
- Add example script and a prompt template
- Define core data models like `HeropromptWorkspace`
This commit is contained in:
Mahmoud-Emad
2025-08-13 20:13:01 +03:00
parent a6d4a23172
commit 14771ed944
9 changed files with 499 additions and 1578 deletions

View File

@@ -0,0 +1,40 @@
module heroprompt
import os
import freeflowuniverse.herolib.core.pathlib
@[params]
pub struct AddFileParams {
pub mut:
name string
}
pub fn (mut dir HeropromptDir) select_file(args AddFileParams) !&HeropromptFile {
mut full_path := dir.path.path + '/' + args.name
if dir.path.path.ends_with('/') {
full_path = dir.path.path + args.name
}
if !os.exists(full_path) {
return error('File ${full_path} does not exists')
}
if !os.is_file(full_path) {
return error('Provided path ${full_path} is not a file')
}
file_content := os.read_file(full_path)!
file := &HeropromptFile{
path: pathlib.Path{
path: full_path
cat: .file
exist: .yes
}
name: args.name
content: file_content
}
dir.files << file
return file
}

View File

@@ -0,0 +1 @@
module heroprompt

View File

@@ -0,0 +1,23 @@
module heroprompt
import rand
pub struct HeropromptSession {
pub mut:
id string
workspaces []&HeropromptWorkspace
}
pub fn new_session() HeropromptSession {
return HeropromptSession{
id: rand.uuid_v4()
workspaces: []
}
}
pub fn (mut self HeropromptSession) add_workspace(args_ NewWorkspaceParams) !&HeropromptWorkspace {
mut wsp := &HeropromptWorkspace{}
wsp = wsp.new(args_)!
self.workspaces << wsp
return wsp
}

View File

@@ -0,0 +1,219 @@
module heroprompt
import rand
import time
import os
import freeflowuniverse.herolib.core.pathlib
@[heap]
pub struct HeropromptWorkspace {
pub mut:
name string = 'default'
dirs []&HeropromptDir
}
@[params]
pub struct NewWorkspaceParams {
pub mut:
name string
}
/// Create a new workspace
/// If the name is not passed, we will generate a random one
fn (wsp HeropromptWorkspace) new(args_ NewWorkspaceParams) !&HeropromptWorkspace {
mut args := args_
if args.name.len == 0 {
args.name = generate_random_workspace_name()
}
workspace := get(name: args.name)!
return workspace
}
@[params]
pub struct AddDirParams {
pub mut:
path string @[required]
}
pub fn (mut wsp HeropromptWorkspace) add_dir(args_ AddDirParams) !&HeropromptDir {
if args_.path.len == 0 {
return error('The dir path is required')
}
if !os.exists(args_.path) {
return error('The provided path does not exists')
}
// Normalize absolute path
abs_path := os.real_path(args_.path)
parts := abs_path.split(os.path_separator)
dir_name := parts[parts.len - 1]
added_dir := &HeropromptDir{
path: pathlib.Path{
path: abs_path
cat: .dir
exist: .yes
}
name: dir_name
}
wsp.dirs << added_dir
return added_dir
}
struct SelectedFilesMetadata {
content_length int
extension string
name string
path string
}
struct SelectedDirsMetadata {
name string
selected_files []SelectedFilesMetadata
}
struct HeropromptWorkspaceGetSelected {
pub mut:
dirs []SelectedDirsMetadata
}
pub fn (wsp HeropromptWorkspace) get_selected() HeropromptWorkspaceGetSelected {
mut result := HeropromptWorkspaceGetSelected{}
for dir in wsp.dirs {
mut files := []SelectedFilesMetadata{}
for file in dir.files {
files << SelectedFilesMetadata{
content_length: file.content.len
extension: get_file_extension(file.name)
name: file.name
path: file.path.path
}
}
result.dirs << SelectedDirsMetadata{
name: dir.name
selected_files: files
}
}
return result
}
pub struct HeropromptWorkspacePrompt {
pub mut:
text string
}
pub fn (wsp HeropromptWorkspace) prompt(args HeropromptWorkspacePrompt) string {
prompt := wsp.build_prompt(args.text)
return prompt
}
// Placeholder function for future needs, in case we need to highlight the user_instructions block with some addtional messages
fn (wsp HeropromptWorkspace) build_user_instructions(text string) string {
return text
}
fn build_file_map(dirs []&HeropromptDir, prefix string) string {
mut out := ''
for i, dir in dirs {
// Determine the correct tree connector
connector := if i == dirs.len - 1 { ' ' } else { ' ' }
// Directory name
out += '${prefix}${connector}${dir.name}\n'
// Files in this directory
for j, file in dir.files {
file_connector := if j == dir.files.len - 1 && dir.dirs.len == 0 {
' '
} else {
' '
}
out += '${prefix} ${file_connector}${file.name} *\n'
}
// Recurse into subdirectories
if dir.dirs.len > 0 {
new_prefix := if i == dirs.len - 1 { prefix + ' ' } else { prefix + ' ' }
out += build_file_map(dir.dirs, new_prefix)
}
}
return out
}
fn (wsp HeropromptWorkspace) build_file_content() string {
return ''
}
fn (wsp HeropromptWorkspace) build_prompt(text string) string {
user_instructions := wsp.build_user_instructions(text)
file_map := build_file_map(wsp.dirs, '')
file_contents := wsp.build_file_content()
// Handle reading the prompt file and parse it
return file_map
}
/// Generate a random name for the workspace
fn generate_random_workspace_name() string {
adjectives := [
'brave',
'bright',
'clever',
'swift',
'noble',
'mighty',
'fearless',
'bold',
'wise',
'epic',
'valiant',
'fierce',
'legendary',
'heroic',
'dynamic',
]
nouns := [
'forge',
'script',
'ocean',
'phoenix',
'atlas',
'quest',
'shield',
'dragon',
'code',
'summit',
'path',
'realm',
'spark',
'anvil',
'saga',
]
// Seed randomness with time
rand.seed([u32(time.now().unix()), u32(time.now().nanosecond)])
adj := adjectives[rand.intn(adjectives.len) or { 0 }]
noun := nouns[rand.intn(nouns.len) or { 0 }]
number := rand.intn(100) or { 0 } // 099
return '${adj}_${noun}_${number}'
}
fn get_file_extension(filename string) string {
parts := filename.split('.')
if parts.len < 2 {
// Handle the files with no exe such as Dockerfile, LICENSE
return ''
}
return parts[parts.len - 1]
}

View File

@@ -10,3 +10,7 @@ fn (mut ws HeropromptWorkspace) heroprompt() !string {
// TODO: fill in template based on selection
return ''
}
pub fn get_tree() {}
pub fn format_prompt() {}

View File

@@ -1,7 +1,7 @@
module heroprompt
import freeflowuniverse.herolib.data.paramsparser
import freeflowuniverse.herolib.data.encoderhero
// import freeflowuniverse.herolib.data.encoderhero // temporarily commented out
import freeflowuniverse.herolib.core.pathlib
import os
@@ -11,38 +11,60 @@ const default = true
// THIS THE THE SOURCE OF THE INFORMATION OF THIS FILE, HERE WE HAVE THE CONFIG OBJECT CONFIGURED AND MODELLED
@[heap]
pub struct HeropromptWorkspace {
pub struct HeropromptFile {
pub mut:
name string = 'default'
dirs []HeropromptDir
content string
path pathlib.Path
name string
}
pub struct HeropromptDir {
pub mut:
path pathlib.Path
selections []string // paths selected in the HeropromptDir
name string
path pathlib.Path
files []&HeropromptFile
dirs []&HeropromptDir
}
// pub fn (wsp HeropromptWorkspace) to_tag() {
// tag := HeropromptTags.file_map
// // We need to pass it to the template
// }
// // pub fn (dir HeropromptDir) to_tag() {
// // tag := HeropromptTags.file_content
// // // We need to pass it to the template
// // }
// pub fn (fil HeropromptFile) to_tag() {
// tag := HeropromptTags.file_content
// // We need to pass it to the template
// }
// pub enum HeropromptTags {
// file_map
// file_content
// user_instructions
// }
// your checking & initialization code if needed
fn obj_init(mycfg_ HeropromptWorkspace) !HeropromptWorkspace {
mut mycfg := mycfg_
if mycfg.password == '' && mycfg.secret == '' {
return error('password or secret needs to be filled in for ${mycfg.name}')
}
return mycfg
}
/////////////NORMALLY NO NEED TO TOUCH
// TODO: Check the compiler issue with the encde/decode
pub fn heroscript_dumps(obj HeropromptWorkspace) !string {
// create heroscript following template
// check for our homedir on our machine and replace in the heroscript to @HOME in path
return encoderhero.encode[HeropromptWorkspace](obj)!
// return encoderhero.encode[HeropromptWorkspace](obj)! // temporarily commented out
return 'name: "${obj.name}"'
}
pub fn heroscript_loads(heroscript string) !HeropromptWorkspace {
// TODO: parse heroscript populate HeropromptWorkspace
mut obj := encoderhero.decode[HeropromptWorkspace](heroscript)!
// mut obj := encoderhero.decode[HeropromptWorkspace](heroscript)! // temporarily commented out
obj := HeropromptWorkspace{
name: 'default'
}
return obj
}

View File

@@ -0,0 +1,11 @@
<user_instructions>
{{text}}
</user_instructions>
<file_map>
{{map}}
</file_map>
<file_contents>
{{content}}
</file_contents>

File diff suppressed because it is too large Load Diff